From 39c36babf0736975230c5541209deb3eb5fdead0 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Thu, 13 Mar 2025 12:14:53 +0000 Subject: [PATCH] Add a script to backup a single blob from a project GitOrigin-RevId: 464e6d69093b87891497e07d1627cd20e2285380 --- .../storage/scripts/backup_blob.mjs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 services/history-v1/storage/scripts/backup_blob.mjs diff --git a/services/history-v1/storage/scripts/backup_blob.mjs b/services/history-v1/storage/scripts/backup_blob.mjs new file mode 100644 index 0000000000..0b05925096 --- /dev/null +++ b/services/history-v1/storage/scripts/backup_blob.mjs @@ -0,0 +1,107 @@ +// @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 { setTimeout } from 'node:timers/promises' + +/** + * Gracefully shutdown the process + * @return {Promise} + */ +async function gracefulShutdown() { + console.log('Gracefully shutting down') + await knex.destroy() + await client.close() + await setTimeout(100) + process.exit() +} + +/** + * + * @return {Promise<{hash: string, historyId: string}>} + */ +async function fetchOptions() { + const { historyId, hash } = commandLineArgs([ + { name: 'historyId', type: String }, + { name: 'hash', type: String }, + ]) + + if (!historyId) { + console.error('historyId is required') + process.exitCode = 1 + await gracefulShutdown() + } + + assert.projectId(historyId) + + if (!hash) { + console.error('hash is required') + process.exitCode = 1 + await gracefulShutdown() + } + + assert.blobHash(hash) + + await loadGlobalBlobs() + + 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} + */ +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 options + +try { + options = await fetchOptions() +} catch (error) { + console.error(error) + await gracefulShutdown() +} + +if (!options) { + // This is mostly to satisfy typescript + process.exitCode = 1 + await gracefulShutdown() + process.exit(1) +} + +try { + const { hash, historyId } = options + await downloadAndBackupBlob(historyId, hash) +} catch (error) { + console.error(error) + process.exitCode = 1 +} finally { + await gracefulShutdown() +}