DocArchiveManager.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. const { callbackify } = require('util')
  2. const MongoManager = require('./MongoManager').promises
  3. const Errors = require('./Errors')
  4. const logger = require('logger-sharelatex')
  5. const settings = require('settings-sharelatex')
  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 DESTROY_BATCH_SIZE = settings.destroyBatchSize
  13. module.exports = {
  14. archiveAllDocs: callbackify(archiveAllDocs),
  15. archiveDocById: callbackify(archiveDocById),
  16. archiveDoc: callbackify(archiveDoc),
  17. unArchiveAllDocs: callbackify(unArchiveAllDocs),
  18. unarchiveDoc: callbackify(unarchiveDoc),
  19. destroyAllDocs: callbackify(destroyAllDocs),
  20. destroyDoc: callbackify(destroyDoc),
  21. promises: {
  22. archiveAllDocs,
  23. archiveDocById,
  24. archiveDoc,
  25. unArchiveAllDocs,
  26. unarchiveDoc,
  27. destroyAllDocs,
  28. destroyDoc
  29. }
  30. }
  31. async function archiveAllDocs(projectId) {
  32. const docs = await MongoManager.getProjectsDocs(
  33. projectId,
  34. { include_deleted: true },
  35. { lines: true, ranges: true, rev: true, inS3: true }
  36. )
  37. if (!docs) {
  38. throw new Errors.NotFoundError(`No docs for project ${projectId}`)
  39. }
  40. const docsToArchive = docs.filter((doc) => !doc.inS3)
  41. await pMap(docsToArchive, (doc) => archiveDoc(projectId, doc), {
  42. concurrency: PARALLEL_JOBS
  43. })
  44. }
  45. async function archiveDocById(projectId, docId) {
  46. const doc = await MongoManager.findDoc(projectId, docId, {
  47. lines: true,
  48. ranges: true,
  49. rev: true,
  50. inS3: true
  51. })
  52. if (!doc) {
  53. throw new Errors.NotFoundError(
  54. `Cannot find doc ${docId} in project ${projectId}`
  55. )
  56. }
  57. // TODO(das7pad): consider refactoring MongoManager.findDoc to take a query
  58. if (doc.inS3) return
  59. return archiveDoc(projectId, doc)
  60. }
  61. async function archiveDoc(projectId, doc) {
  62. logger.log(
  63. { project_id: projectId, doc_id: doc._id },
  64. 'sending doc to persistor'
  65. )
  66. const key = `${projectId}/${doc._id}`
  67. if (doc.lines == null) {
  68. throw new Error('doc has no lines')
  69. }
  70. const json = JSON.stringify({
  71. lines: doc.lines,
  72. ranges: doc.ranges,
  73. schema_v: 1
  74. })
  75. // this should never happen, but protects against memory-corruption errors that
  76. // have happened in the past
  77. if (json.indexOf('\u0000') > -1) {
  78. const error = new Error('null bytes detected')
  79. logger.err({ err: error, doc }, error.message)
  80. throw error
  81. }
  82. const md5 = crypto.createHash('md5').update(json).digest('hex')
  83. const stream = Streamifier.createReadStream(json)
  84. await PersistorManager.sendStream(settings.docstore.bucket, key, stream, {
  85. sourceMd5: md5
  86. })
  87. await MongoManager.markDocAsArchived(doc._id, doc.rev)
  88. }
  89. async function unArchiveAllDocs(projectId) {
  90. let docs
  91. if (settings.docstore.keepSoftDeletedDocsArchived) {
  92. docs = await MongoManager.getNonDeletedArchivedProjectDocs(projectId)
  93. } else {
  94. docs = await MongoManager.getArchivedProjectDocs(projectId)
  95. }
  96. if (!docs) {
  97. throw new Errors.NotFoundError(`No docs for project ${projectId}`)
  98. }
  99. await pMap(docs, (doc) => unarchiveDoc(projectId, doc._id), {
  100. concurrency: PARALLEL_JOBS
  101. })
  102. }
  103. async function unarchiveDoc(projectId, docId) {
  104. logger.log(
  105. { project_id: projectId, doc_id: docId },
  106. 'getting doc from persistor'
  107. )
  108. const originalDoc = await MongoManager.findDoc(projectId, docId, { inS3: 1 })
  109. if (!originalDoc.inS3) {
  110. // return if it's not actually in S3 as there's nothing to do
  111. return
  112. }
  113. const key = `${projectId}/${docId}`
  114. let stream, sourceMd5
  115. try {
  116. sourceMd5 = await PersistorManager.getObjectMd5Hash(
  117. settings.docstore.bucket,
  118. key
  119. )
  120. stream = await PersistorManager.getObjectStream(
  121. settings.docstore.bucket,
  122. key
  123. )
  124. } catch (err) {
  125. // if we get a 404, we could be in a race and something else has unarchived the doc already
  126. if (err instanceof Errors.NotFoundError) {
  127. const doc = await MongoManager.findDoc(projectId, docId, { inS3: 1 })
  128. if (!doc.inS3) {
  129. // the doc has been archived while we were looking for it, so no error
  130. return
  131. }
  132. }
  133. throw err
  134. }
  135. stream.resume()
  136. const json = await _streamToString(stream)
  137. const md5 = crypto.createHash('md5').update(json).digest('hex')
  138. if (sourceMd5 !== md5) {
  139. throw new Errors.Md5MismatchError('md5 mismatch when downloading doc', {
  140. key,
  141. sourceMd5,
  142. md5
  143. })
  144. }
  145. const doc = JSON.parse(json)
  146. const mongoDoc = {}
  147. if (doc.schema_v === 1 && doc.lines != null) {
  148. mongoDoc.lines = doc.lines
  149. if (doc.ranges != null) {
  150. mongoDoc.ranges = RangeManager.jsonRangesToMongo(doc.ranges)
  151. }
  152. } else if (Array.isArray(doc)) {
  153. mongoDoc.lines = doc
  154. } else {
  155. throw new Error("I don't understand the doc format in s3")
  156. }
  157. await MongoManager.upsertIntoDocCollection(projectId, docId, mongoDoc)
  158. await PersistorManager.deleteObject(settings.docstore.bucket, key)
  159. }
  160. async function destroyAllDocs(projectId) {
  161. while (true) {
  162. const docs = await MongoManager.getProjectsDocs(
  163. projectId,
  164. { include_deleted: true, limit: DESTROY_BATCH_SIZE },
  165. { _id: 1 }
  166. )
  167. if (!docs || docs.length === 0) {
  168. break
  169. }
  170. await pMap(docs, (doc) => destroyDoc(projectId, doc._id), {
  171. concurrency: PARALLEL_JOBS
  172. })
  173. }
  174. }
  175. async function destroyDoc(projectId, docId) {
  176. logger.log(
  177. { project_id: projectId, doc_id: docId },
  178. 'removing doc from mongo and persistor'
  179. )
  180. const doc = await MongoManager.findDoc(projectId, docId, {
  181. inS3: 1
  182. })
  183. if (!doc) {
  184. throw new Errors.NotFoundError('Doc not found in Mongo')
  185. }
  186. if (doc.inS3) {
  187. await PersistorManager.deleteObject(
  188. settings.docstore.bucket,
  189. `${projectId}/${docId}`
  190. )
  191. }
  192. await MongoManager.destroyDoc(docId)
  193. }
  194. async function _streamToString(stream) {
  195. const chunks = []
  196. return new Promise((resolve, reject) => {
  197. stream.on('data', (chunk) => chunks.push(chunk))
  198. stream.on('error', reject)
  199. stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
  200. })
  201. }