ZipManager.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. const logger = require('@overleaf/logger')
  2. const UpdatesManager = require('./UpdatesManager')
  3. const DiffGenerator = require('./DiffGenerator')
  4. const DocumentUpdaterManager = require('./DocumentUpdaterManager')
  5. const DocstoreManager = require('./DocstoreManager')
  6. const Errors = require('./Errors')
  7. const PackManager = require('./PackManager')
  8. const yazl = require('yazl')
  9. const util = require('util')
  10. const stream = require('stream')
  11. const fs = require('fs')
  12. const os = require('os')
  13. const Path = require('path')
  14. const { packsAreDuplicated } = require('./util/PackUtils')
  15. const streamPipeline = util.promisify(stream.pipeline)
  16. // look in docstore or docupdater for the latest version of the document
  17. async function getLatestContent(projectId, docId, lastUpdateVersion) {
  18. const [docstoreContent, docstoreVersion] =
  19. await DocstoreManager.promises.peekDocument(projectId, docId)
  20. // if docstore is out of date, check for a newer version in docupdater
  21. // and return that instead
  22. if (docstoreVersion <= lastUpdateVersion) {
  23. const [docupdaterContent, docupdaterVersion] =
  24. await DocumentUpdaterManager.promises.peekDocument(projectId, docId)
  25. if (docupdaterVersion > docstoreVersion) {
  26. return [docupdaterContent, docupdaterVersion]
  27. }
  28. }
  29. return [docstoreContent, docstoreVersion]
  30. }
  31. async function rewindDoc(projectId, docId, zipfile) {
  32. logger.debug({ projectId, docId }, 'rewinding document')
  33. // Prepare to rewind content
  34. const docIterator = await PackManager.promises.makeDocIterator(docId)
  35. const getUpdate = util.promisify(docIterator.next).bind(docIterator)
  36. const lastUpdate = await getUpdate()
  37. if (!lastUpdate) {
  38. return null
  39. }
  40. const lastUpdateVersion = lastUpdate.v
  41. let latestContent
  42. let version
  43. try {
  44. ;[latestContent, version] = await getLatestContent(
  45. projectId,
  46. docId,
  47. lastUpdateVersion
  48. )
  49. } catch (err) {
  50. if (err instanceof Errors.NotFoundError) {
  51. // Doc not found in docstore. We can't build its history
  52. return null
  53. } else {
  54. throw err
  55. }
  56. }
  57. const id = docId.toString()
  58. const contentEndPath = `${id}/content/end/${version}`
  59. zipfile.addBuffer(Buffer.from(latestContent), contentEndPath)
  60. const metadata = {
  61. id,
  62. version,
  63. content: {
  64. end: {
  65. path: contentEndPath,
  66. version,
  67. },
  68. },
  69. updates: [],
  70. }
  71. let content = latestContent
  72. let v = version
  73. let update = lastUpdate
  74. let previousUpdate = null
  75. while (update) {
  76. if (packsAreDuplicated(update, previousUpdate)) {
  77. previousUpdate = update
  78. update = await getUpdate()
  79. continue
  80. }
  81. const updatePath = `${id}/updates/${update.v}`
  82. try {
  83. content = DiffGenerator.rewindUpdate(content, update)
  84. // filter out any known "broken ops" as these may be recoverable
  85. update.op = update.op.filter(op => !op.broken)
  86. // only store the update in the zip file when we have applied it
  87. // successfully, and after filtering out broken ops.
  88. zipfile.addBuffer(Buffer.from(JSON.stringify(update)), updatePath, {
  89. mtime: new Date(update.meta.start_ts),
  90. })
  91. v = update.v
  92. } catch (e) {
  93. e.attempted_update = update // keep a record of the attempted update
  94. logger.warn({ projectId, docId, err: e }, 'rewind error')
  95. break // stop attempting to rewind on error
  96. }
  97. metadata.updates.push({
  98. path: updatePath,
  99. version: update.v,
  100. ts: update.meta.start_ts,
  101. doc_length: content.length,
  102. })
  103. previousUpdate = update
  104. update = await getUpdate()
  105. }
  106. const contentStartPath = `${id}/content/start/${v}`
  107. zipfile.addBuffer(Buffer.from(content), contentStartPath)
  108. metadata.content.start = {
  109. path: contentStartPath,
  110. version: v,
  111. }
  112. return metadata
  113. }
  114. async function generateZip(projectId, zipfile) {
  115. await UpdatesManager.promises.processUncompressedUpdatesForProject(projectId)
  116. const docIds = await PackManager.promises.findAllDocsInProject(projectId)
  117. const manifest = { projectId, docs: [] }
  118. for (const docId of docIds) {
  119. const doc = await rewindDoc(projectId, docId, zipfile)
  120. if (doc) {
  121. manifest.docs.push(doc)
  122. }
  123. }
  124. zipfile.addBuffer(
  125. Buffer.from(JSON.stringify(manifest, null, 2)),
  126. 'manifest.json'
  127. )
  128. zipfile.end()
  129. }
  130. async function exportProject(projectId, zipPath) {
  131. const zipfile = new yazl.ZipFile()
  132. const pipeline = streamPipeline(
  133. zipfile.outputStream,
  134. fs.createWriteStream(zipPath)
  135. )
  136. await generateZip(projectId, zipfile)
  137. await pipeline
  138. }
  139. /**
  140. * Create a temporary directory for use with exportProject()
  141. */
  142. async function makeTempDirectory() {
  143. const tmpdir = await fs.promises.mkdtemp(
  144. (await fs.promises.realpath(os.tmpdir())) + Path.sep
  145. )
  146. return tmpdir
  147. }
  148. /**
  149. * Clean up a temporary directory made with makeTempDirectory()
  150. */
  151. function cleanupTempDirectory(tmpdir) {
  152. fs.promises.rm(tmpdir, { recursive: true, force: true }).catch(err => {
  153. if (err) {
  154. logger.warn({ err, tmpdir }, 'Failed to clean up temp directory')
  155. }
  156. })
  157. }
  158. module.exports = {
  159. exportProject: util.callbackify(exportProject),
  160. makeTempDirectory: util.callbackify(makeTempDirectory),
  161. cleanupTempDirectory,
  162. }