recover_zip_from_backup.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // @ts-check
  2. import { loadGlobalBlobs } from '../lib/blob_store/index.js'
  3. import commandLineArgs from 'command-line-args'
  4. import assert from '../lib/assert.js'
  5. import fs from 'node:fs'
  6. import { setTimeout } from 'node:timers/promises'
  7. import { pipeline } from 'node:stream/promises'
  8. import {
  9. archiveLatestChunk,
  10. archiveRawProject,
  11. BackupPersistorError,
  12. } from '../lib/backupArchiver.mjs'
  13. import knex from '../lib/knex.js'
  14. import { client } from '../lib/mongodb.js'
  15. import ZipStream from 'zip-stream'
  16. import { Chunk } from 'overleaf-editor-core'
  17. import _ from 'lodash'
  18. const SUPPORTED_MODES = ['raw', 'latest']
  19. // Pads the mode name to a fixed length for alignment.
  20. const padModeName = _.partialRight(
  21. _.padEnd,
  22. Math.max(...SUPPORTED_MODES.map(mode => mode.length))
  23. )
  24. const SUPPORTED_MODES_HELP = {
  25. raw: 'Retrieve all chunk and blob files from the project backup.',
  26. latest: 'Retrieves the last backed up state of the project.',
  27. }
  28. // outputFile needs to be available in the shutdown function (which may be called before it's declared)
  29. // eslint-disable-next-line prefer-const
  30. let outputFile
  31. /**
  32. * Gracefully shutdown the process
  33. * @param {number} code
  34. */
  35. async function shutdown(code = 0) {
  36. if (outputFile) {
  37. outputFile.close()
  38. }
  39. await knex.destroy()
  40. await client.close()
  41. await setTimeout(1000)
  42. process.exit(code)
  43. }
  44. function usage() {
  45. console.log(
  46. 'Usage: node recover_zip_from_backup.mjs --historyId=<historyId> --output=<output> [--mode=<mode>] [--verbose] [--useBackupGlobalBlobs]'
  47. )
  48. console.log(
  49. '--useBackupGlobalBlobs can be used if the global blobs have not been restored from the backup yet.'
  50. )
  51. console.log('Supported modes: ' + SUPPORTED_MODES.join(', '))
  52. SUPPORTED_MODES.forEach(mode => {
  53. console.log(
  54. ` --mode=${padModeName(mode)} - ${SUPPORTED_MODES_HELP[mode] || ''}`
  55. )
  56. })
  57. }
  58. let historyId, help, mode, output, useBackupGlobalBlobs, verbose
  59. try {
  60. ;({ historyId, help, mode, output, useBackupGlobalBlobs, verbose } =
  61. commandLineArgs([
  62. { name: 'historyId', type: String },
  63. { name: 'output', type: String },
  64. { name: 'mode', type: String, defaultValue: 'raw' },
  65. { name: 'verbose', type: Boolean, defaultValue: false },
  66. { name: 'useBackupGlobalBlobs', type: Boolean, defaultValue: false },
  67. { name: 'help', type: Boolean },
  68. ]))
  69. } catch (err) {
  70. console.error(err instanceof Error ? err.message : err)
  71. help = true
  72. }
  73. if (help) {
  74. usage()
  75. await shutdown(0)
  76. }
  77. if (!historyId) {
  78. console.error('missing --historyId')
  79. usage()
  80. await shutdown(1)
  81. }
  82. if (!output) {
  83. console.error('missing --output')
  84. usage()
  85. await shutdown(1)
  86. }
  87. try {
  88. assert.projectId(historyId)
  89. } catch (error) {
  90. console.error('Invalid history ID')
  91. await shutdown(1)
  92. }
  93. if (!SUPPORTED_MODES.includes(mode)) {
  94. console.error(
  95. 'Invalid mode; supported modes are: ' + SUPPORTED_MODES.join(', ')
  96. )
  97. await shutdown(1)
  98. }
  99. await loadGlobalBlobs()
  100. outputFile = fs.createWriteStream(output)
  101. const archive = new ZipStream()
  102. archive.on('error', function (e) {
  103. console.error(`Error writing archive: ${e.message}`)
  104. })
  105. try {
  106. // Pipe archive to the output file before adding entries.
  107. // pipeline handles backpressure and will resolve when
  108. // the archive stream ends.
  109. const pipelinePromise = pipeline(archive, outputFile)
  110. switch (mode) {
  111. case 'latest':
  112. await archiveLatestChunk(
  113. archive,
  114. historyId,
  115. useBackupGlobalBlobs,
  116. verbose
  117. )
  118. break
  119. case 'raw':
  120. default:
  121. await archiveRawProject(archive, historyId, useBackupGlobalBlobs, verbose)
  122. break
  123. }
  124. archive.finalize()
  125. await pipelinePromise
  126. console.log(`Wrote ${archive.getBytesWritten()} total bytes to ${output}`)
  127. } catch (error) {
  128. if (error instanceof BackupPersistorError) {
  129. console.error(error.message)
  130. }
  131. if (error instanceof Chunk.NotPersistedError) {
  132. console.error('Chunk not found. Project may not have been fully backed up.')
  133. }
  134. if (verbose) {
  135. console.error(error)
  136. } else {
  137. console.error('Error encountered when writing archive')
  138. }
  139. await shutdown(1)
  140. }
  141. await shutdown(0)