DocArchiveManager.js 5.9 KB

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