ZipManager.js 4.3 KB

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