backup_blob.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // @ts-check
  2. import commandLineArgs from 'command-line-args'
  3. import { backupBlob, downloadBlobToDir } from '../lib/backupBlob.mjs'
  4. import withTmpDir from '../../api/controllers/with_tmp_dir.js'
  5. import {
  6. BlobStore,
  7. GLOBAL_BLOBS,
  8. loadGlobalBlobs,
  9. } from '../lib/blob_store/index.js'
  10. import assert from '../lib/assert.js'
  11. import knex from '../lib/knex.js'
  12. import { client } from '../lib/mongodb.js'
  13. import redis from '../lib/redis.js'
  14. import { setTimeout } from 'node:timers/promises'
  15. import fs from 'node:fs'
  16. await loadGlobalBlobs()
  17. /**
  18. * Gracefully shutdown the process
  19. * @return {Promise<void>}
  20. */
  21. async function gracefulShutdown() {
  22. console.log('Gracefully shutting down')
  23. await knex.destroy()
  24. await client.close()
  25. await redis.disconnect()
  26. await setTimeout(100)
  27. process.exit()
  28. }
  29. /**
  30. *
  31. * @param {string} row
  32. * @return {BackupBlobJob}
  33. */
  34. function parseCSVRow(row) {
  35. const [historyId, hash] = row.split(',')
  36. validateBackedUpBlobJob({ historyId, hash })
  37. return { historyId, hash }
  38. }
  39. /**
  40. *
  41. * @param {BackupBlobJob} job
  42. */
  43. function validateBackedUpBlobJob(job) {
  44. assert.projectId(job.historyId)
  45. assert.blobHash(job.hash)
  46. }
  47. /**
  48. *
  49. * @param {string} path
  50. * @return {Promise<Array<BackupBlobJob>>}
  51. */
  52. async function readCSV(path) {
  53. let fh
  54. /** @type {Array<BackupBlobJob>} */
  55. const rows = []
  56. try {
  57. fh = await fs.promises.open(path, 'r')
  58. } catch (error) {
  59. console.error(`Could not open file: ${error}`)
  60. throw error
  61. }
  62. for await (const line of fh.readLines()) {
  63. try {
  64. const row = parseCSVRow(line)
  65. if (GLOBAL_BLOBS.has(row.hash)) {
  66. console.log(`Skipping global blob: ${line}`)
  67. continue
  68. }
  69. rows.push(row)
  70. } catch (error) {
  71. console.error(error instanceof Error ? error.message : error)
  72. console.log(`Skipping invalid row: ${line}`)
  73. }
  74. }
  75. return rows
  76. }
  77. /**
  78. * @typedef {Object} BackupBlobJob
  79. * @property {string} hash
  80. * @property {string} historyId
  81. */
  82. /**
  83. * @param {Object} options
  84. * @property {string} [options.historyId]
  85. * @property {string} [options.hash]
  86. * @property {string} [options.input]
  87. * @return {Promise<Array<BackupBlobJob>>}
  88. */
  89. async function initialiseJobs({ historyId, hash, input }) {
  90. if (input) {
  91. return await readCSV(input)
  92. }
  93. if (!historyId) {
  94. console.error('historyId is required')
  95. process.exitCode = 1
  96. await gracefulShutdown()
  97. }
  98. if (!hash) {
  99. console.error('hash is required')
  100. process.exitCode = 1
  101. await gracefulShutdown()
  102. }
  103. validateBackedUpBlobJob({ historyId, hash })
  104. if (GLOBAL_BLOBS.has(hash)) {
  105. console.error(`Blob ${hash} is a global blob; not backing up`)
  106. process.exitCode = 1
  107. await gracefulShutdown()
  108. }
  109. return [{ hash, historyId }]
  110. }
  111. /**
  112. *
  113. * @param {string} historyId
  114. * @param {string} hash
  115. * @return {Promise<void>}
  116. */
  117. export async function downloadAndBackupBlob(historyId, hash) {
  118. const blobStore = new BlobStore(historyId)
  119. const blob = await blobStore.getBlob(hash)
  120. if (!blob) {
  121. throw new Error(`Blob ${hash} could not be loaded`)
  122. }
  123. await withTmpDir(`blob-${hash}`, async tmpDir => {
  124. const filePath = await downloadBlobToDir(historyId, blob, tmpDir)
  125. console.log(`Downloaded blob ${hash} to ${filePath}`)
  126. await backupBlob(historyId, blob, filePath)
  127. console.log('Backed up blob')
  128. })
  129. }
  130. let jobs
  131. const options = commandLineArgs([
  132. { name: 'historyId', type: String },
  133. { name: 'hash', type: String },
  134. { name: 'input', type: String },
  135. ])
  136. try {
  137. jobs = await initialiseJobs(options)
  138. } catch (error) {
  139. console.error(error)
  140. await gracefulShutdown()
  141. }
  142. if (!Array.isArray(jobs)) {
  143. // This is mostly to satisfy typescript
  144. process.exitCode = 1
  145. await gracefulShutdown()
  146. process.exit(1)
  147. }
  148. for (const { historyId, hash } of jobs) {
  149. try {
  150. await downloadAndBackupBlob(historyId, hash)
  151. } catch (error) {
  152. console.error(error)
  153. process.exitCode = 1
  154. }
  155. }
  156. await gracefulShutdown()