DocArchiveManager.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. archiveDoc: callbackify(archiveDoc),
  15. unArchiveAllDocs: callbackify(unArchiveAllDocs),
  16. unarchiveDoc: callbackify(unarchiveDoc),
  17. destroyAllDocs: callbackify(destroyAllDocs),
  18. destroyDoc: callbackify(destroyDoc),
  19. promises: {
  20. archiveAllDocs,
  21. archiveDoc,
  22. unArchiveAllDocs,
  23. unarchiveDoc,
  24. destroyAllDocs,
  25. destroyDoc
  26. }
  27. }
  28. async function archiveAllDocs(projectId) {
  29. const docs = await MongoManager.getProjectsDocs(
  30. projectId,
  31. { include_deleted: true },
  32. { lines: true, ranges: true, rev: true, inS3: true }
  33. )
  34. if (!docs) {
  35. throw new Errors.NotFoundError(`No docs for project ${projectId}`)
  36. }
  37. const docsToArchive = docs.filter((doc) => !doc.inS3)
  38. await pMap(docsToArchive, (doc) => archiveDoc(projectId, doc), {
  39. concurrency: PARALLEL_JOBS
  40. })
  41. }
  42. async function archiveDoc(projectId, doc) {
  43. logger.log(
  44. { project_id: projectId, doc_id: doc._id },
  45. 'sending doc to persistor'
  46. )
  47. const key = `${projectId}/${doc._id}`
  48. if (doc.lines == null) {
  49. throw new Error('doc has no lines')
  50. }
  51. const json = JSON.stringify({
  52. lines: doc.lines,
  53. ranges: doc.ranges,
  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(doc._id, doc.rev)
  69. }
  70. async function unArchiveAllDocs(projectId) {
  71. const docs = await MongoManager.getArchivedProjectDocs(projectId)
  72. if (!docs) {
  73. throw new Errors.NotFoundError(`No docs for project ${projectId}`)
  74. }
  75. await pMap(docs, (doc) => unarchiveDoc(projectId, doc._id), {
  76. concurrency: PARALLEL_JOBS
  77. })
  78. }
  79. async function unarchiveDoc(projectId, docId) {
  80. logger.log(
  81. { project_id: projectId, doc_id: docId },
  82. 'getting doc from persistor'
  83. )
  84. const originalDoc = await MongoManager.findDoc(projectId, docId, { inS3: 1 })
  85. if (!originalDoc.inS3) {
  86. // return if it's not actually in S3 as there's nothing to do
  87. return
  88. }
  89. const key = `${projectId}/${docId}`
  90. let stream, sourceMd5
  91. try {
  92. sourceMd5 = await PersistorManager.getObjectMd5Hash(
  93. settings.docstore.bucket,
  94. key
  95. )
  96. stream = await PersistorManager.getObjectStream(
  97. settings.docstore.bucket,
  98. key
  99. )
  100. } catch (err) {
  101. // if we get a 404, we could be in a race and something else has unarchived the doc already
  102. if (err instanceof Errors.NotFoundError) {
  103. const doc = await MongoManager.findDoc(projectId, docId, { inS3: 1 })
  104. if (!doc.inS3) {
  105. // the doc has been archived while we were looking for it, so no error
  106. return
  107. }
  108. }
  109. throw err
  110. }
  111. stream.resume()
  112. const json = await _streamToString(stream)
  113. const md5 = crypto.createHash('md5').update(json).digest('hex')
  114. if (sourceMd5 !== md5) {
  115. throw new Errors.Md5MismatchError('md5 mismatch when downloading doc', {
  116. key,
  117. sourceMd5,
  118. md5
  119. })
  120. }
  121. const doc = JSON.parse(json)
  122. const mongoDoc = {}
  123. if (doc.schema_v === 1 && doc.lines != null) {
  124. mongoDoc.lines = doc.lines
  125. if (doc.ranges != null) {
  126. mongoDoc.ranges = RangeManager.jsonRangesToMongo(doc.ranges)
  127. }
  128. } else if (Array.isArray(doc)) {
  129. mongoDoc.lines = doc
  130. } else {
  131. throw new Error("I don't understand the doc format in s3")
  132. }
  133. await MongoManager.upsertIntoDocCollection(projectId, docId, mongoDoc)
  134. await PersistorManager.deleteObject(settings.docstore.bucket, key)
  135. }
  136. async function destroyAllDocs(projectId) {
  137. const docs = await MongoManager.getProjectsDocs(
  138. projectId,
  139. { include_deleted: true },
  140. { _id: 1 }
  141. )
  142. if (docs && docs.length) {
  143. await pMap(docs, (doc) => destroyDoc(projectId, doc._id), {
  144. concurrency: PARALLEL_JOBS
  145. })
  146. }
  147. }
  148. async function destroyDoc(projectId, docId) {
  149. logger.log(
  150. { project_id: projectId, doc_id: docId },
  151. 'removing doc from mongo and persistor'
  152. )
  153. const doc = await MongoManager.findDoc(projectId, docId, {
  154. inS3: 1
  155. })
  156. if (!doc) {
  157. throw new Errors.NotFoundError('Doc not found in Mongo')
  158. }
  159. if (doc.inS3) {
  160. await PersistorManager.deleteObject(
  161. settings.docstore.bucket,
  162. `${projectId}/${docId}`
  163. )
  164. }
  165. await MongoManager.destroyDoc(docId)
  166. }
  167. async function _streamToString(stream) {
  168. const chunks = []
  169. return new Promise((resolve, reject) => {
  170. stream.on('data', (chunk) => chunks.push(chunk))
  171. stream.on('error', reject)
  172. stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
  173. })
  174. }