DocArchiveManager.js 5.9 KB

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