DocArchiveManager.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import MongoManager from './MongoManager.js'
  2. import Errors from './Errors.js'
  3. import logger from '@overleaf/logger'
  4. import Settings from '@overleaf/settings'
  5. import crypto from 'node:crypto'
  6. import { ReadableString } from '@overleaf/stream-utils'
  7. import RangeManager from './RangeManager.js'
  8. import PersistorManager from './PersistorManager.js'
  9. import pMap from 'p-map'
  10. import { streamToBuffer } from './StreamToBuffer.js'
  11. import mongodb from 'mongodb-legacy'
  12. const { BSON } = mongodb
  13. const PARALLEL_JOBS = Settings.parallelArchiveJobs
  14. const UN_ARCHIVE_BATCH_SIZE = Settings.unArchiveBatchSize
  15. async function archiveAllDocs(projectId) {
  16. if (!_isArchivingEnabled()) {
  17. return
  18. }
  19. const docIds = await MongoManager.getNonArchivedProjectDocIds(projectId)
  20. await pMap(docIds, docId => archiveDoc(projectId, docId), {
  21. concurrency: PARALLEL_JOBS,
  22. })
  23. }
  24. async function archiveDoc(projectId, docId) {
  25. if (!_isArchivingEnabled()) {
  26. return
  27. }
  28. const doc = await MongoManager.getDocForArchiving(projectId, docId)
  29. if (!doc) {
  30. // The doc wasn't found, it was already archived, or the lock couldn't be
  31. // acquired. Since we don't know which it is, silently return.
  32. return
  33. }
  34. logger.debug({ projectId, docId: doc._id }, 'sending doc to persistor')
  35. const key = `${projectId}/${doc._id}`
  36. if (doc.lines == null) {
  37. throw new Error('doc has no lines')
  38. }
  39. RangeManager.fixCommentIds(doc)
  40. // warn about any oversized docs already in mongo
  41. const linesSize = BSON.calculateObjectSize(doc.lines || {})
  42. const rangesSize = BSON.calculateObjectSize(doc.ranges || {})
  43. if (
  44. linesSize > Settings.max_doc_length ||
  45. rangesSize > Settings.max_doc_length
  46. ) {
  47. logger.warn(
  48. { projectId, docId: doc._id, linesSize, rangesSize },
  49. 'large doc found when archiving project'
  50. )
  51. }
  52. const json = JSON.stringify({
  53. lines: doc.lines,
  54. ranges: doc.ranges,
  55. rev: doc.rev,
  56. schema_v: 1,
  57. })
  58. // this should never happen, but protects against memory-corruption errors that
  59. // have happened in the past
  60. if (json.indexOf('\u0000') > -1) {
  61. const error = new Error('null bytes detected')
  62. logger.err({ err: error, doc }, error.message)
  63. throw error
  64. }
  65. const stream = new ReadableString(json)
  66. if (Settings.docstore.backend === 's3') {
  67. await PersistorManager.sendStream(Settings.docstore.bucket, key, stream)
  68. } else {
  69. await PersistorManager.sendStream(Settings.docstore.bucket, key, stream, {
  70. sourceMd5: crypto.createHash('md5').update(json).digest('hex'),
  71. })
  72. }
  73. await MongoManager.markDocAsArchived(projectId, docId, doc.rev)
  74. }
  75. async function unArchiveAllDocs(projectId) {
  76. if (!_isArchivingEnabled()) {
  77. return
  78. }
  79. while (true) {
  80. let docs
  81. if (Settings.docstore.keepSoftDeletedDocsArchived) {
  82. docs = await MongoManager.getNonDeletedArchivedProjectDocs(
  83. projectId,
  84. UN_ARCHIVE_BATCH_SIZE
  85. )
  86. } else {
  87. docs = await MongoManager.getArchivedProjectDocs(
  88. projectId,
  89. UN_ARCHIVE_BATCH_SIZE
  90. )
  91. }
  92. if (!docs || docs.length === 0) {
  93. break
  94. }
  95. await pMap(docs, doc => unarchiveDoc(projectId, doc._id), {
  96. concurrency: PARALLEL_JOBS,
  97. })
  98. }
  99. }
  100. // get the doc from the PersistorManager without storing it in mongo
  101. async function getDoc(projectId, docId) {
  102. const key = `${projectId}/${docId}`
  103. const stream = await PersistorManager.getObjectStream(
  104. Settings.docstore.bucket,
  105. key
  106. )
  107. let buffer
  108. if (Settings.docstore.backend === 's3') {
  109. stream.resume()
  110. buffer = await streamToBuffer(projectId, docId, stream)
  111. } else {
  112. const sourceMd5 = await PersistorManager.getObjectMd5Hash(
  113. Settings.docstore.bucket,
  114. key
  115. )
  116. stream.resume()
  117. buffer = await streamToBuffer(projectId, docId, stream)
  118. const md5 = crypto.createHash('md5').update(buffer).digest('hex')
  119. if (sourceMd5 !== md5) {
  120. throw new Errors.Md5MismatchError('md5 mismatch when downloading doc', {
  121. key,
  122. sourceMd5,
  123. md5,
  124. })
  125. }
  126. }
  127. return _deserializeArchivedDoc(buffer)
  128. }
  129. // get the doc and unarchive it to mongo
  130. async function unarchiveDoc(projectId, docId) {
  131. logger.debug({ projectId, docId }, 'getting doc from persistor')
  132. const mongoDoc = await MongoManager.findDoc(projectId, docId, {
  133. inS3: 1,
  134. rev: 1,
  135. })
  136. if (!mongoDoc.inS3) {
  137. // The doc is already unarchived
  138. return
  139. }
  140. if (!_isArchivingEnabled()) {
  141. throw new Error(
  142. 'found archived doc, but archiving backend is not configured'
  143. )
  144. }
  145. const archivedDoc = await getDoc(projectId, docId)
  146. if (archivedDoc.rev == null) {
  147. // Older archived docs didn't have a rev. Assume that the rev of the
  148. // archived doc is the rev that was stored in Mongo when we retrieved it
  149. // earlier.
  150. archivedDoc.rev = mongoDoc.rev
  151. }
  152. await MongoManager.restoreArchivedDoc(projectId, docId, archivedDoc)
  153. }
  154. async function destroyProject(projectId) {
  155. const tasks = [MongoManager.destroyProject(projectId)]
  156. if (_isArchivingEnabled()) {
  157. tasks.push(
  158. PersistorManager.deleteDirectory(Settings.docstore.bucket, projectId)
  159. )
  160. }
  161. await Promise.all(tasks)
  162. }
  163. function _deserializeArchivedDoc(buffer) {
  164. const doc = JSON.parse(buffer)
  165. const result = {}
  166. if (doc.schema_v === 1 && doc.lines != null) {
  167. result.lines = doc.lines
  168. if (doc.ranges != null) {
  169. result.ranges = RangeManager.jsonRangesToMongo(doc.ranges)
  170. }
  171. } else if (Array.isArray(doc)) {
  172. result.lines = doc
  173. } else {
  174. throw new Error("I don't understand the doc format in s3")
  175. }
  176. if (doc.rev != null) {
  177. result.rev = doc.rev
  178. }
  179. return result
  180. }
  181. function _isArchivingEnabled() {
  182. const backend = Settings.docstore.backend
  183. if (!backend) {
  184. return false
  185. }
  186. // The default backend is S3. If another backend is configured or the S3
  187. // backend itself is correctly configured, then archiving is enabled.
  188. if (backend === 's3' && Settings.docstore.s3 == null) {
  189. return false
  190. }
  191. return true
  192. }
  193. export default {
  194. archiveAllDocs,
  195. archiveDoc,
  196. unArchiveAllDocs,
  197. unarchiveDoc,
  198. destroyProject,
  199. getDoc,
  200. }