DocArchiveManager.js 5.5 KB

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