| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- // @ts-check
- import commandLineArgs from 'command-line-args'
- import { backupBlob, downloadBlobToDir } from '../lib/backupBlob.mjs'
- import withTmpDir from '../../api/controllers/with_tmp_dir.js'
- import {
- BlobStore,
- GLOBAL_BLOBS,
- loadGlobalBlobs,
- } from '../lib/blob_store/index.js'
- import assert from '../lib/assert.js'
- import knex from '../lib/knex.js'
- import { client } from '../lib/mongodb.js'
- import redis from '../lib/redis.js'
- import { setTimeout } from 'node:timers/promises'
- import fs from 'node:fs'
- await loadGlobalBlobs()
- /**
- * Gracefully shutdown the process
- * @return {Promise<void>}
- */
- async function gracefulShutdown() {
- console.log('Gracefully shutting down')
- await knex.destroy()
- await client.close()
- await redis.disconnect()
- await setTimeout(100)
- process.exit()
- }
- /**
- *
- * @param {string} row
- * @return {BackupBlobJob}
- */
- function parseCSVRow(row) {
- const [historyId, hash] = row.split(',')
- validateBackedUpBlobJob({ historyId, hash })
- return { historyId, hash }
- }
- /**
- *
- * @param {BackupBlobJob} job
- */
- function validateBackedUpBlobJob(job) {
- assert.projectId(job.historyId)
- assert.blobHash(job.hash)
- }
- /**
- *
- * @param {string} path
- * @return {Promise<Array<BackupBlobJob>>}
- */
- async function readCSV(path) {
- let fh
- /** @type {Array<BackupBlobJob>} */
- const rows = []
- try {
- fh = await fs.promises.open(path, 'r')
- } catch (error) {
- console.error(`Could not open file: ${error}`)
- throw error
- }
- for await (const line of fh.readLines()) {
- try {
- const row = parseCSVRow(line)
- if (GLOBAL_BLOBS.has(row.hash)) {
- console.log(`Skipping global blob: ${line}`)
- continue
- }
- rows.push(row)
- } catch (error) {
- console.error(error instanceof Error ? error.message : error)
- console.log(`Skipping invalid row: ${line}`)
- }
- }
- return rows
- }
- /**
- * @typedef {Object} BackupBlobJob
- * @property {string} hash
- * @property {string} historyId
- */
- /**
- * @param {Object} options
- * @property {string} [options.historyId]
- * @property {string} [options.hash]
- * @property {string} [options.input]
- * @return {Promise<Array<BackupBlobJob>>}
- */
- async function initialiseJobs({ historyId, hash, input }) {
- if (input) {
- return await readCSV(input)
- }
- if (!historyId) {
- console.error('historyId is required')
- process.exitCode = 1
- await gracefulShutdown()
- }
- if (!hash) {
- console.error('hash is required')
- process.exitCode = 1
- await gracefulShutdown()
- }
- validateBackedUpBlobJob({ historyId, hash })
- if (GLOBAL_BLOBS.has(hash)) {
- console.error(`Blob ${hash} is a global blob; not backing up`)
- process.exitCode = 1
- await gracefulShutdown()
- }
- return [{ hash, historyId }]
- }
- /**
- *
- * @param {string} historyId
- * @param {string} hash
- * @return {Promise<void>}
- */
- export async function downloadAndBackupBlob(historyId, hash) {
- const blobStore = new BlobStore(historyId)
- const blob = await blobStore.getBlob(hash)
- if (!blob) {
- throw new Error(`Blob ${hash} could not be loaded`)
- }
- await withTmpDir(`blob-${hash}`, async tmpDir => {
- const filePath = await downloadBlobToDir(historyId, blob, tmpDir)
- console.log(`Downloaded blob ${hash} to ${filePath}`)
- await backupBlob(historyId, blob, filePath)
- console.log('Backed up blob')
- })
- }
- let jobs
- const options = commandLineArgs([
- { name: 'historyId', type: String },
- { name: 'hash', type: String },
- { name: 'input', type: String },
- ])
- try {
- jobs = await initialiseJobs(options)
- } catch (error) {
- console.error(error)
- await gracefulShutdown()
- }
- if (!Array.isArray(jobs)) {
- // This is mostly to satisfy typescript
- process.exitCode = 1
- await gracefulShutdown()
- process.exit(1)
- }
- for (const { historyId, hash } of jobs) {
- try {
- await downloadAndBackupBlob(historyId, hash)
- } catch (error) {
- console.error(error)
- process.exitCode = 1
- }
- }
- await gracefulShutdown()
|