UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

226 lines (187 loc) 6.96 kB
import {type CliCommandDefinition, type CliPrompter} from '@sanity/cli' import exportDataset from '@sanity/export' import {absolutify} from '@sanity/util/fs' import fs from 'fs/promises' import path from 'path' import prettyMs from 'pretty-ms' import {chooseDatasetPrompt} from '../../actions/dataset/chooseDatasetPrompt' import {validateDatasetName} from '../../actions/dataset/validateDatasetName' const noop = () => null const helpText = ` Options --raw Extract only documents, without rewriting asset references --no-assets Export only non-asset documents and remove references to image assets --no-drafts Export only published versions of documents --no-compress Skips compressing tarball entries (still generates a gzip file) --types Defines which document types to export --overwrite Overwrite any file with the same name --asset-concurrency <num> Concurrent number of asset downloads Examples sanity dataset export moviedb localPath.tar.gz sanity dataset export moviedb assetless.tar.gz --no-assets sanity dataset export staging staging.tar.gz --raw sanity dataset export staging staging.tar.gz --types products,shops ` interface ExportFlags { raw?: boolean assets?: boolean drafts?: boolean compress?: boolean overwrite?: boolean types?: string 'asset-concurrency'?: string } interface ParsedExportFlags { raw?: boolean assets?: boolean drafts?: boolean compress?: boolean overwrite?: boolean types?: string[] assetConcurrency?: number } function parseFlags(rawFlags: ExportFlags): ParsedExportFlags { const flags: ParsedExportFlags = {} if (rawFlags.types) { flags.types = `${rawFlags.types}`.split(',') } if (rawFlags['asset-concurrency']) { flags.assetConcurrency = parseInt(rawFlags['asset-concurrency'], 10) } if (typeof rawFlags.raw !== 'undefined') { flags.raw = Boolean(rawFlags.raw) } if (typeof rawFlags.assets !== 'undefined') { flags.assets = Boolean(rawFlags.assets) } if (typeof rawFlags.drafts !== 'undefined') { flags.drafts = Boolean(rawFlags.drafts) } if (typeof rawFlags.compress !== 'undefined') { flags.compress = Boolean(rawFlags.compress) } if (typeof rawFlags.overwrite !== 'undefined') { flags.overwrite = Boolean(rawFlags.overwrite) } return flags } interface ProgressEvent { step: string update?: boolean current: number total: number } const exportDatasetCommand: CliCommandDefinition<ExportFlags> = { name: 'export', group: 'dataset', signature: '[NAME] [DESTINATION]', description: 'Export dataset to local filesystem as a gzipped tarball', helpText, action: async (args, context) => { const {apiClient, output, chalk, workDir, prompt} = context const client = apiClient() const [targetDataset, targetDestination] = args.argsWithoutOptions const flags = parseFlags(args.extOptions) let dataset = targetDataset ? `${targetDataset}` : null if (!dataset) { dataset = await chooseDatasetPrompt(context, {message: 'Select dataset to export'}) } const dsError = validateDatasetName(dataset) if (dsError) { throw dsError } // Verify existence of dataset before trying to export from it const datasets = await client.datasets.list() if (!datasets.find((set) => set.name === dataset)) { throw new Error(`Dataset with name "${dataset}" not found`) } // Print information about what projectId and dataset it is being exported from const {projectId} = client.config() output.print('╭───────────────────────────────────────────────╮') output.print('│ │') output.print('│ Exporting from: │') output.print(`│ ${chalk.bold('projectId')}: ${chalk.cyan(projectId).padEnd(44)} │`) output.print(`│ ${chalk.bold('dataset')}: ${chalk.cyan(dataset).padEnd(46)} │`) output.print('│ │') output.print('╰───────────────────────────────────────────────╯') output.print('') let destinationPath = targetDestination if (!destinationPath) { destinationPath = await prompt.single({ type: 'input', message: 'Output path:', default: path.join(workDir, `${dataset}.tar.gz`), filter: absolutify, }) } const outputPath = await getOutputPath(destinationPath, dataset, prompt, flags) if (!outputPath) { output.print('Cancelled') return } // If we are dumping to a file, let the user know where it's at if (outputPath !== '-') { output.print(`Exporting dataset "${chalk.cyan(dataset)}" to "${chalk.cyan(outputPath)}"`) } let currentStep = 'Exporting documents...' let spinner = output.spinner(currentStep).start() const onProgress = (progress: ProgressEvent) => { if (progress.step !== currentStep) { spinner.succeed() spinner = output.spinner(progress.step).start() } else if (progress.step === currentStep && progress.update) { spinner.text = `${progress.step} (${progress.current}/${progress.total})` } currentStep = progress.step } const start = Date.now() try { await exportDataset({ client, dataset, outputPath, onProgress, ...flags, }) spinner.succeed() } catch (err) { spinner.fail() throw err } output.print(`Export finished (${prettyMs(Date.now() - start)})`) }, } // eslint-disable-next-line complexity async function getOutputPath( destination: string, dataset: string, prompt: CliPrompter, flags: ParsedExportFlags, ) { if (destination === '-') { return '-' } const dstPath = path.isAbsolute(destination) ? destination : path.resolve(process.cwd(), destination) let dstStats = await fs.stat(dstPath).catch(noop) const looksLikeFile = dstStats ? dstStats.isFile() : path.basename(dstPath).indexOf('.') !== -1 if (!dstStats) { const createPath = looksLikeFile ? path.dirname(dstPath) : dstPath await fs.mkdir(createPath, {recursive: true}) } const finalPath = looksLikeFile ? dstPath : path.join(dstPath, `${dataset}.tar.gz`) dstStats = await fs.stat(finalPath).catch(noop) if (!flags.overwrite && dstStats && dstStats.isFile()) { const shouldOverwrite = await prompt.single({ type: 'confirm', message: `File "${finalPath}" already exists, would you like to overwrite it?`, default: false, }) if (!shouldOverwrite) { return false } } return finalPath } export default exportDatasetCommand