UNPKG

datastore-backup

Version:

Programatic Backup of Google Cloud Datastore

145 lines (134 loc) 4.63 kB
import path from 'path'; import { Datastore } from '@google-cloud/datastore'; import ora, { Ora } from 'ora'; import prettyBytes from 'pretty-bytes'; import * as R from 'ramda'; export async function dumpAllKinds( datastore: Datastore, bucketName: string, backupName?: string, backupDir = 'bak', spinner?: Ora ) { spinner = spinner || ora({ isSilent: true }); const outputUrls: string[] = []; backupName = backupName || (await getBackupDefaultName(datastore)); spinner.info(`backup to gs://${bucketName}/${backupDir}/${backupName}`); spinner.info( `Dumping datastore ${await datastore.getProjectId()}` + (datastore.namespace ? `for namespace ${datastore.namespace}` : '') ); try { spinner.start(`Loading Kinds`); const kindNames = await getKindNames(datastore); spinner.succeed(`${kindNames.length} Kinds`).start(); for (const sublist of R.splitEvery(100, kindNames)) { spinner.start(); const { outputUrl } = await dumpKinds( datastore, sublist, bucketName, backupDir, `${backupName}-${outputUrls.length}`, spinner ); outputUrls.push(outputUrl); } spinner.start().succeed(`written ${outputUrls}`); } catch (error) { spinner.fail(error.message); throw error; } return outputUrls; } async function getBackupDefaultName(datastore: Datastore) { return ( new Date() .toISOString() .replaceAll(/[-:Z]/g, '') .replaceAll(/\.\d+$/g, '') + '-' + (await datastore.getProjectId()) + (datastore.namespace ? ':' + datastore.namespace : '') ); } async function dumpKinds( datastore: Datastore, kindNames: string[], backupBucket: string, backupDir: string, backupName: string, spinner?: any ) { spinner = spinner || ora({ isSilent: true }); const infoText = kindNames.length > 1 ? `${kindNames.length} kinds ` : kindNames[0]; spinner.prefixText = infoText; spinner.start(); const backupSubDirName = kindNames.length > 1 ? backupName : path.join(backupName, kindNames[0]); const outputUrlPrefix = `gs://${path.join( backupBucket, backupDir, backupSubDirName )}`; spinner.info(`Dumping ${kindNames.join(', ')}`); spinner.info(`Dumping to ${outputUrlPrefix}`).start(); const logProgress = (status) => { // The counters are https://github.com/dcodeIO/long.js // const eD: number = status.progressEntities.workCompleted.toNumber(); // const eT: number = status.progressEntities.workEstimated.toNumber(); const bD: number = status.progressBytes.workCompleted.toNumber(); const bT: number = status.progressBytes.workEstimated.toNumber(); spinner.text = `${prettyBytes(bD)} of ${prettyBytes(bT)} ${( (bD / bT) * 100 ).toFixed(1)}%`; }; // https://cloud.google.com/datastore/docs/reference/admin/rest/v1/projects/export try { const [operation] = await datastore.export({ outputUrlPrefix, kinds: kindNames, namespaces: [datastore.namespace], }); // see https://googleapis.github.io/gax-nodejs/classes/Operation.html#promise // operation emits 'progress', 'error' and 'complete' operation.on('progress', logProgress); const response = await operation.promise(); const meta = response[1]; const timeUsed = meta.common.endTime.seconds - meta.common.startTime.seconds; spinner.succeed( `Dumping finished ${meta?.progressEntities?.workCompleted?.toNumber()} records (${prettyBytes( meta?.progressBytes?.workCompleted?.toNumber() )}) in ${timeUsed}s` ); return { outputUrl: response[0].outputUrl, operation: operation, result: response, }; } catch (error) { spinner.fail(error.message); throw error; } } /** Returns a list of all Namespaces in a Datastore for the current namespace. */ export async function getNamespaces(datastore: Datastore): Promise<string[]> { const query = datastore.createQuery('__namespace__').select('__key__'); const [entities] = await datastore.runQuery(query); const namespaces = entities.map((entity) => entity[datastore.KEY].name); return namespaces; } /** Returns a list of all Kinds in a Datastore for the current namespace. * * Kinds where the name starts with `_` are supressed. */ async function getKindNames(datastore: Datastore): Promise<string[]> { const query = datastore.createQuery('__kind__').select('__key__'); const [entities] = await datastore.runQuery(query); const kinds = entities.map((entity) => entity[datastore.KEY].name); return kinds.filter((x) => !x.startsWith('_')); }