From cd7ed7db4715b8668416e3536116e124b94c66ff Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Wed, 20 Jan 2021 23:51:49 +0100 Subject: [PATCH] :sparkles: Add CLI export/import for credentials/workflows (#1363) * Added options to import and export workflows and credentials * Fixed linting issues and descriptions * Linting issues and allowing import/export operations to output multiple files * :zap: Minor changes to CLI export/import Co-authored-by: Jan Oberhauser --- packages/cli/commands/export/credentials.ts | 125 ++++++++++++++++++++ packages/cli/commands/import/credentials.ts | 75 ++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 packages/cli/commands/export/credentials.ts create mode 100644 packages/cli/commands/import/credentials.ts diff --git a/packages/cli/commands/export/credentials.ts b/packages/cli/commands/export/credentials.ts new file mode 100644 index 0000000000..24168a5a1d --- /dev/null +++ b/packages/cli/commands/export/credentials.ts @@ -0,0 +1,125 @@ +import { + Command, flags, +} from '@oclif/command'; + +import { + IDataObject +} from 'n8n-workflow'; + +import { + Db, + GenericHelpers, +} from '../../src'; + +import * as fs from 'fs'; +import * as path from 'path'; + +export class ExportCredentialsCommand extends Command { + static description = 'Export credentials'; + + static examples = [ + `$ n8n export:credentials --all`, + `$ n8n export:credentials --id=5 --output=file.json`, + `$ n8n export:credentials --all --output=backups/latest/`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + all: flags.boolean({ + description: 'Export all credentials', + }), + id: flags.string({ + description: 'The ID of the credential to export', + }), + output: flags.string({ + description: 'Output file name or directory if using separate files', + }), + pretty: flags.boolean({ + description: 'Format the output in an easier to read fashion', + }), + separate: flags.boolean({ + description: 'Exports one file per credential (useful for versioning). Must inform a directory via --output.', + }), + }; + + async run() { + const { flags } = this.parse(ExportCredentialsCommand); + + if (!flags.all && !flags.id) { + GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`); + return; + } + + if (flags.all && flags.id) { + GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`); + return; + } + + if (flags.separate) { + try { + if (!flags.output) { + GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`); + return; + } + + if (fs.existsSync(flags.output)) { + if (!fs.lstatSync(flags.output).isDirectory()) { + GenericHelpers.logOutput(`The paramenter --output must be a directory`); + return; + } + } else { + fs.mkdirSync(flags.output, { recursive: true }); + } + } catch (e) { + console.error('\nFILESYSTEM ERROR'); + console.log('===================================='); + console.error(e.message); + console.error(e.stack); + this.exit(1); + } + } else if (flags.output) { + if (fs.existsSync(flags.output)) { + if (fs.lstatSync(flags.output).isDirectory()) { + GenericHelpers.logOutput(`The paramenter --output must be a writeble file`); + return; + } + } + } + + try { + await Db.init(); + + const findQuery: IDataObject = {}; + if (flags.id) { + findQuery.id = flags.id; + } + + const credentials = await Db.collections.Credentials!.find(findQuery); + + if (credentials.length === 0) { + throw new Error('No credentials found with specified filters.'); + } + + if (flags.separate) { + let fileContents: string, i: number; + for (i = 0; i < credentials.length; i++) { + fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined); + const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + ".json"; + fs.writeFileSync(filename, fileContents); + } + console.log('Successfully exported', i, 'credentials.'); + } else { + const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined); + if (flags.output) { + fs.writeFileSync(flags.output!, fileContents); + console.log('Successfully exported', credentials.length, 'credentials.'); + } else { + console.log(fileContents); + } + } + } catch (error) { + this.error(error.message); + this.exit(1); + } + } +} diff --git a/packages/cli/commands/import/credentials.ts b/packages/cli/commands/import/credentials.ts new file mode 100644 index 0000000000..3b30077431 --- /dev/null +++ b/packages/cli/commands/import/credentials.ts @@ -0,0 +1,75 @@ +import { + Command, flags, +} from '@oclif/command'; + +import { + Db, + GenericHelpers, +} from '../../src'; + +import * as fs from 'fs'; +import * as glob from 'glob-promise'; +import * as path from 'path'; + +export class ImportCredentialsCommand extends Command { + static description = 'Import credentials'; + + static examples = [ + `$ n8n import:credentials --input=file.json`, + `$ n8n import:credentials --separate --input=backups/latest/`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + input: flags.string({ + description: 'Input file name or directory if --separate is used', + }), + separate: flags.boolean({ + description: 'Imports *.json files from directory provided by --input', + }), + }; + + async run() { + const { flags } = this.parse(ImportCredentialsCommand); + + if (!flags.input) { + GenericHelpers.logOutput(`An input file or directory with --input must be provided`); + return; + } + + if (flags.separate) { + if (fs.existsSync(flags.input)) { + if (!fs.lstatSync(flags.input).isDirectory()) { + GenericHelpers.logOutput(`The paramenter --input must be a directory`); + return; + } + } + } + + try { + await Db.init(); + let i; + if (flags.separate) { + const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json'); + for (i = 0; i < files.length; i++) { + const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' })); + await Db.collections.Credentials!.save(credential); + } + } else { + const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' })); + + if (!Array.isArray(fileContents)) { + throw new Error(`File does not seem to contain credentials.`); + } + + for (i = 0; i < fileContents.length; i++) { + await Db.collections.Credentials!.save(fileContents[i]); + } + } + console.log('Successfully imported', i, 'credentials.'); + } catch (error) { + this.error(error.message); + this.exit(1); + } + } +}