DocArchiveManager.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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 asyncPool = require('tiny-async-pool')
  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. await asyncPool(
  38. PARALLEL_JOBS,
  39. docs.filter((doc) => !doc.inS3),
  40. (doc) => archiveDoc(projectId, doc)
  41. )
  42. }
  43. async function archiveDoc(projectId, doc) {
  44. logger.log(
  45. { project_id: projectId, doc_id: doc._id },
  46. 'sending doc to persistor'
  47. )
  48. const key = `${projectId}/${doc._id}`
  49. if (doc.lines == null) {
  50. throw new Error('doc has no lines')
  51. }
  52. const json = JSON.stringify({
  53. lines: doc.lines,
  54. ranges: doc.ranges,
  55. schema_v: 1
  56. })
  57. // this should never happen, but protects against memory-corruption errors that
  58. // have happened in the past
  59. if (json.indexOf('\u0000') > -1) {
  60. const error = new Error('null bytes detected')
  61. logger.err({ err: error, doc }, error.message)
  62. throw error
  63. }
  64. const md5 = crypto.createHash('md5').update(json).digest('hex')
  65. const stream = Streamifier.createReadStream(json)
  66. await PersistorManager.sendStream(settings.docstore.bucket, key, stream, {
  67. sourceMd5: md5
  68. })
  69. await MongoManager.markDocAsArchived(doc._id, doc.rev)
  70. }
  71. async function unArchiveAllDocs(projectId) {
  72. const docs = await MongoManager.getArchivedProjectDocs(projectId)
  73. if (!docs) {
  74. throw new Errors.NotFoundError(`No docs for project ${projectId}`)
  75. }
  76. if (!docs.length) {
  77. // asyncPool will throw an error with an empty array
  78. return
  79. }
  80. await asyncPool(PARALLEL_JOBS, docs, (doc) =>
  81. unarchiveDoc(projectId, doc._id)
  82. )
  83. }
  84. async function unarchiveDoc(projectId, docId) {
  85. logger.log(
  86. { project_id: projectId, doc_id: docId },
  87. 'getting doc from persistor'
  88. )
  89. const key = `${projectId}/${docId}`
  90. const sourceMd5 = await PersistorManager.getObjectMd5Hash(
  91. settings.docstore.bucket,
  92. key
  93. )
  94. const stream = await PersistorManager.getObjectStream(
  95. settings.docstore.bucket,
  96. key
  97. )
  98. stream.resume()
  99. const json = await _streamToString(stream)
  100. const md5 = crypto.createHash('md5').update(json).digest('hex')
  101. if (sourceMd5 !== md5) {
  102. throw new Errors.Md5MismatchError('md5 mismatch when downloading doc', {
  103. key,
  104. sourceMd5,
  105. md5
  106. })
  107. }
  108. const doc = JSON.parse(json)
  109. const mongoDoc = {}
  110. if (doc.schema_v === 1 && doc.lines != null) {
  111. mongoDoc.lines = doc.lines
  112. if (doc.ranges != null) {
  113. mongoDoc.ranges = RangeManager.jsonRangesToMongo(doc.ranges)
  114. }
  115. } else if (Array.isArray(doc)) {
  116. mongoDoc.lines = doc
  117. } else {
  118. throw new Error("I don't understand the doc format in s3")
  119. }
  120. await MongoManager.upsertIntoDocCollection(projectId, docId, mongoDoc)
  121. await PersistorManager.deleteObject(settings.docstore.bucket, key)
  122. }
  123. async function destroyAllDocs(projectId) {
  124. const docs = await MongoManager.getProjectsDocs(
  125. projectId,
  126. { include_deleted: true },
  127. { _id: 1 }
  128. )
  129. if (docs && docs.length) {
  130. await asyncPool(PARALLEL_JOBS, docs, (doc) =>
  131. destroyDoc(projectId, doc._id)
  132. )
  133. }
  134. }
  135. async function destroyDoc(projectId, docId) {
  136. logger.log(
  137. { project_id: projectId, doc_id: docId },
  138. 'removing doc from mongo and persistor'
  139. )
  140. const doc = await MongoManager.findDoc(projectId, docId, {
  141. inS3: 1
  142. })
  143. if (!doc) {
  144. throw new Errors.NotFoundError('Doc not found in Mongo')
  145. }
  146. if (doc.inS3) {
  147. await PersistorManager.deleteObject(
  148. settings.docstore.bucket,
  149. `${projectId}/${docId}`
  150. )
  151. }
  152. await MongoManager.destroyDoc(docId)
  153. }
  154. async function _streamToString(stream) {
  155. const chunks = []
  156. return new Promise((resolve, reject) => {
  157. stream.on('data', (chunk) => chunks.push(chunk))
  158. stream.on('error', reject)
  159. stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
  160. })
  161. }