diff --git a/packages/cli/commands/export/credentials.ts b/packages/cli/commands/export/credentials.ts index b944a96f70..6f9038e6cd 100644 --- a/packages/cli/commands/export/credentials.ts +++ b/packages/cli/commands/export/credentials.ts @@ -22,6 +22,7 @@ export class ExportCredentialsCommand extends Command { `$ n8n export:credentials --all`, `$ n8n export:credentials --id=5 --output=file.json`, `$ n8n export:credentials --all --output=backups/latest/`, + `$ n8n export:credentials --backup --output=backups/latest/`, ]; static flags = { @@ -29,10 +30,14 @@ export class ExportCredentialsCommand extends Command { all: flags.boolean({ description: 'Export all credentials', }), + backup: flags.boolean({ + description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.', + }), id: flags.string({ description: 'The ID of the credential to export', }), output: flags.string({ + char: 'o', description: 'Output file name or directory if using separate files', }), pretty: flags.boolean({ @@ -46,6 +51,12 @@ export class ExportCredentialsCommand extends Command { async run() { const { flags } = this.parse(ExportCredentialsCommand); + if (flags.backup) { + flags.all = true; + flags.pretty = true; + flags.separate = true; + } + if (!flags.all && !flags.id) { GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`); return; diff --git a/packages/cli/commands/export/workflow.ts b/packages/cli/commands/export/workflow.ts new file mode 100644 index 0000000000..33066fd380 --- /dev/null +++ b/packages/cli/commands/export/workflow.ts @@ -0,0 +1,137 @@ +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 ExportWorkflowsCommand extends Command { + static description = 'Export workflows'; + + static examples = [ + `$ n8n export:workflow --all`, + `$ n8n export:workflow --id=5 --output=file.json`, + `$ n8n export:workflow --all --output=backups/latest/`, + `$ n8n export:workflow --backup --output=backups/latest/`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + all: flags.boolean({ + description: 'Export all workflows', + }), + backup: flags.boolean({ + description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.', + }), + id: flags.string({ + description: 'The ID of the workflow to export', + }), + output: flags.string({ + char: 'o', + 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 workflow (useful for versioning). Must inform a directory via --output.', + }), + }; + + async run() { + const { flags } = this.parse(ExportWorkflowsCommand); + + if (flags.backup) { + flags.all = true; + flags.pretty = true; + flags.separate = true; + } + + 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 workflows = await Db.collections.Workflow!.find(findQuery); + + if (workflows.length === 0) { + throw new Error('No workflows found with specified filters.'); + } + + if (flags.separate) { + let fileContents: string, i: number; + for (i = 0; i < workflows.length; i++) { + fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined); + const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + ".json"; + fs.writeFileSync(filename, fileContents); + } + console.log('Successfully exported', i, 'workflows.'); + } else { + const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined); + if (flags.output) { + fs.writeFileSync(flags.output!, fileContents); + console.log('Successfully exported', workflows.length, workflows.length === 1 ? 'workflow.' : 'workflows.'); + } 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 index 9a75a5ed84..9d109c5b03 100644 --- a/packages/cli/commands/import/credentials.ts +++ b/packages/cli/commands/import/credentials.ts @@ -23,6 +23,7 @@ export class ImportCredentialsCommand extends Command { static flags = { help: flags.help({ char: 'h' }), input: flags.string({ + char: 'i', description: 'Input file name or directory if --separate is used', }), separate: flags.boolean({ diff --git a/packages/cli/commands/import/workflow.ts b/packages/cli/commands/import/workflow.ts new file mode 100644 index 0000000000..7a0dc49e10 --- /dev/null +++ b/packages/cli/commands/import/workflow.ts @@ -0,0 +1,78 @@ +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 ImportWorkflowsCommand extends Command { + static description = 'Import workflows'; + + static examples = [ + `$ n8n import:workflow --input=file.json`, + `$ n8n import:workflow --separate --input=backups/latest/`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + input: flags.string({ + char: 'i', + 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(ImportWorkflowsCommand); + + 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 workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' })); + await Db.collections.Workflow!.save(workflow); + } + } else { + const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' })); + + if (!Array.isArray(fileContents)) { + throw new Error(`File does not seem to contain workflows.`); + } + + for (i = 0; i < fileContents.length; i++) { + await Db.collections.Workflow!.save(fileContents[i]); + } + } + + console.log('Successfully imported', i, i === 1 ? 'workflow.' : 'workflows.'); + } catch (error) { + this.error(error.message); + this.exit(1); + } + } +}