DocArchiveManager.js 6.0 KB

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