ZipManager.js 4.6 KB

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