DocArchiveManager.js 6.1 KB

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