n8n-nodes-base
Version:
Base nodes of n8n
836 lines • 36.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Ftp = void 0;
const fs_1 = require("fs");
const n8n_workflow_1 = require("n8n-workflow");
const path_1 = require("path");
const promise_ftp_1 = __importDefault(require("promise-ftp"));
const ssh2_sftp_client_1 = __importDefault(require("ssh2-sftp-client"));
const promises_1 = require("stream/promises");
const tmp_promise_1 = require("tmp-promise");
const utilities_1 = require("../../utils/utilities");
async function callRecursiveList(path, client, normalizeFunction) {
const pathArray = [path];
let currentPath = path;
const directoryItems = [];
let index = 0;
const prepareAndNormalize = (item) => {
if (pathArray[index].endsWith('/')) {
currentPath = `${pathArray[index]}${item.name}`;
}
else {
currentPath = `${pathArray[index]}/${item.name}`;
}
// Is directory
if (item.type === 'd') {
// ignore . and .. to prevent infinite loop
if (item.name === '.' || item.name === '..') {
return;
}
pathArray.push(currentPath);
}
normalizeFunction(item, currentPath, true);
directoryItems.push(item);
};
do {
const returnData = await client.list(pathArray[index]);
// @ts-ignore
returnData.map(prepareAndNormalize);
index++;
} while (index <= pathArray.length - 1);
return directoryItems;
}
async function recursivelyCreateSftpDirs(sftp, path) {
const dirPath = (0, path_1.dirname)(path);
const dirExists = await sftp.exists(dirPath);
if (!dirExists) {
await sftp.mkdir(dirPath, true);
}
}
function normalizeSFtpItem(input, path, recursive = false) {
const item = input;
item.accessTime = new Date(input.accessTime);
item.modifyTime = new Date(input.modifyTime);
item.path = !recursive ? `${path}${path.endsWith('/') ? '' : '/'}${item.name}` : path;
}
function normalizeFtpItem(input, path, recursive = false) {
const item = input;
item.modifyTime = input.date;
item.path = !recursive ? `${path}${path.endsWith('/') ? '' : '/'}${item.name}` : path;
//@ts-ignore
item.date = undefined;
}
const timeoutOption = {
displayName: 'Timeout',
name: 'timeout',
description: 'Connection timeout in milliseconds',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 10000,
};
class Ftp {
description = {
displayName: 'FTP',
name: 'ftp',
icon: 'fa:server',
iconColor: 'dark-blue',
group: ['input'],
version: 1,
subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}',
description: 'Transfer files via FTP or SFTP',
defaults: {
name: 'FTP',
color: '#303050',
},
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
credentials: [
{
// nodelinter-ignore-next-line
name: 'ftp',
required: true,
displayOptions: {
show: {
protocol: ['ftp'],
},
},
testedBy: 'ftpConnectionTest',
},
{
// nodelinter-ignore-next-line
name: 'sftp',
required: true,
displayOptions: {
show: {
protocol: ['sftp'],
},
},
testedBy: 'sftpConnectionTest',
},
],
properties: [
{
displayName: 'Protocol',
name: 'protocol',
type: 'options',
options: [
{
name: 'FTP',
value: 'ftp',
},
{
name: 'SFTP',
value: 'sftp',
},
],
default: 'ftp',
description: 'File transfer protocol',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a file/folder',
action: 'Delete a file or folder',
},
{
name: 'Download',
value: 'download',
description: 'Download a file',
action: 'Download a file',
},
{
name: 'List',
value: 'list',
description: 'List folder content',
action: 'List folder content',
},
{
name: 'Rename',
value: 'rename',
description: 'Rename/move oldPath to newPath',
action: 'Rename / move a file or folder',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload a file',
action: 'Upload a file',
},
],
default: 'download',
noDataExpression: true,
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: ['delete'],
},
},
name: 'path',
type: 'string',
default: '',
description: 'The file path of the file to delete. Has to contain the full path.',
placeholder: 'e.g. /public/documents/file-to-delete.txt',
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: {},
options: [
{
displayName: 'Folder',
name: 'folder',
type: 'boolean',
default: false,
description: 'Whether folders can be deleted',
},
{
displayName: 'Recursive',
displayOptions: {
show: {
folder: [true],
},
},
name: 'recursive',
type: 'boolean',
default: false,
description: 'Whether to remove all files and directories in target directory',
},
timeoutOption,
],
},
// ----------------------------------
// download
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: ['download'],
},
},
name: 'path',
type: 'string',
default: '',
description: 'The file path of the file to download. Has to contain the full path.',
placeholder: 'e.g. /public/documents/file-to-download.txt',
required: true,
},
{
displayName: 'Put Output File in Field',
displayOptions: {
show: {
operation: ['download'],
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
hint: 'The name of the output binary field to put the file in',
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['download'],
},
},
options: [
{
displayName: 'Enable Concurrent Reads',
name: 'enableConcurrentReads',
type: 'boolean',
default: false,
description: 'Whether to enable concurrent reads for downloading files',
},
{
displayName: 'Max Concurrent Reads',
name: 'maxConcurrentReads',
type: 'number',
default: 5,
displayOptions: {
show: {
enableConcurrentReads: [true],
},
},
},
{
displayName: 'Chunk Size',
name: 'chunkSize',
type: 'number',
default: 64,
description: 'Size of each chunk in KB to download, Not all servers support this',
displayOptions: {
show: {
enableConcurrentReads: [true],
},
},
},
timeoutOption,
],
},
// ----------------------------------
// rename
// ----------------------------------
{
displayName: 'Old Path',
displayOptions: {
show: {
operation: ['rename'],
},
},
name: 'oldPath',
type: 'string',
default: '',
placeholder: 'e.g. /public/documents/old-file.txt',
required: true,
},
{
displayName: 'New Path',
displayOptions: {
show: {
operation: ['rename'],
},
},
name: 'newPath',
type: 'string',
default: '',
placeholder: 'e.g. /public/documents/new-file.txt',
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['rename'],
},
},
options: [
{
displayName: 'Create Directories',
name: 'createDirectories',
type: 'boolean',
default: false,
description: 'Whether to recursively create destination directory when renaming an existing file or folder',
},
timeoutOption,
],
},
// ----------------------------------
// upload
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: ['upload'],
},
},
name: 'path',
type: 'string',
default: '',
description: 'The file path of the file to upload. Has to contain the full path.',
placeholder: 'e.g. /public/documents/file-to-upload.txt',
required: true,
},
{
displayName: 'Binary File',
displayOptions: {
show: {
operation: ['upload'],
},
},
name: 'binaryData',
type: 'boolean',
default: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description: 'The text content of the file to upload',
},
{
displayName: 'Input Binary Field',
displayOptions: {
show: {
operation: ['upload'],
binaryData: [true],
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
hint: 'The name of the input binary field containing the file to be written',
required: true,
},
{
displayName: 'File Content',
displayOptions: {
show: {
operation: ['upload'],
binaryData: [false],
},
},
name: 'fileContent',
type: 'string',
default: '',
description: 'The text content of the file to upload',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['upload'],
},
},
options: [timeoutOption],
},
// ----------------------------------
// list
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: ['list'],
},
},
name: 'path',
type: 'string',
default: '/',
placeholder: 'e.g. /public/folder',
description: 'Path of directory to list contents of',
required: true,
},
{
displayName: 'Recursive',
displayOptions: {
show: {
operation: ['list'],
},
},
name: 'recursive',
type: 'boolean',
default: false,
description: 'Whether to return object representing all directories / objects recursively found within SFTP server',
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['list'],
},
},
options: [timeoutOption],
},
],
};
methods = {
credentialTest: {
async ftpConnectionTest(credential) {
const credentials = credential.data;
const ftp = new promise_ftp_1.default();
try {
await ftp.connect({
host: credentials.host,
port: credentials.port,
user: credentials.username,
password: credentials.password,
});
}
catch (error) {
await ftp.end();
return {
status: 'Error',
message: error.message,
};
}
await ftp.end();
return {
status: 'OK',
message: 'Connection successful!',
};
},
async sftpConnectionTest(credential) {
const credentials = credential.data;
const sftp = new ssh2_sftp_client_1.default();
try {
if (credentials.privateKey) {
await sftp.connect({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password || undefined,
privateKey: (0, utilities_1.formatPrivateKey)(credentials.privateKey),
passphrase: credentials.passphrase,
});
}
else {
await sftp.connect({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password,
});
}
}
catch (error) {
await sftp.end();
return {
status: 'Error',
message: error.message,
};
}
await sftp.end();
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
};
async execute() {
const items = this.getInputData();
let returnItems = [];
const operation = this.getNodeParameter('operation', 0);
let credentials = undefined;
const protocol = this.getNodeParameter('protocol', 0);
const connectionTimeout = this.getNodeParameter('options.timeout', 0, 10000);
if (protocol === 'sftp') {
credentials = await this.getCredentials('sftp');
}
else {
credentials = await this.getCredentials('ftp');
}
let ftp;
let sftp;
try {
try {
if (protocol === 'sftp') {
sftp = new ssh2_sftp_client_1.default();
if (credentials.privateKey) {
await sftp.connect({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password || undefined,
privateKey: (0, utilities_1.formatPrivateKey)(credentials.privateKey),
passphrase: credentials.passphrase,
readyTimeout: connectionTimeout,
algorithms: {
compress: ['zlib@openssh.com', 'zlib', 'none'],
},
});
}
else {
await sftp.connect({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password,
readyTimeout: connectionTimeout,
algorithms: {
compress: ['zlib@openssh.com', 'zlib', 'none'],
},
});
}
}
else {
ftp = new promise_ftp_1.default();
await ftp.connect({
host: credentials.host,
port: credentials.port,
user: credentials.username,
password: credentials.password,
connTimeout: connectionTimeout,
});
}
}
catch (error) {
if (this.continueOnFail()) {
const pairedItem = (0, utilities_1.generatePairedItemData)(items.length);
return [[{ json: { error: error.message }, pairedItem }]];
}
throw error;
}
for (let i = 0; i < items.length; i++) {
try {
const newItem = {
json: items[i].json,
binary: {},
pairedItem: items[i].pairedItem,
};
if (items[i].binary !== undefined && newItem.binary) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
if (protocol === 'sftp') {
if (operation === 'list') {
const path = this.getNodeParameter('path', i);
const recursive = this.getNodeParameter('recursive', i);
let responseData;
if (recursive) {
responseData = await callRecursiveList(path, sftp, normalizeSFtpItem);
}
else {
responseData = await sftp.list(path);
responseData.forEach((item) => normalizeSFtpItem(item, path));
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
if (operation === 'delete') {
const path = this.getNodeParameter('path', i);
const options = this.getNodeParameter('options', i);
if (options.folder === true) {
await sftp.rmdir(path, !!options.recursive);
}
else {
await sftp.delete(path);
}
const executionData = this.helpers.constructExecutionMetaData([{ json: { success: true } }], { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
if (operation === 'rename') {
const oldPath = this.getNodeParameter('oldPath', i);
const { createDirectories = false } = this.getNodeParameter('options', i);
const newPath = this.getNodeParameter('newPath', i);
if (createDirectories) {
await recursivelyCreateSftpDirs(sftp, newPath);
}
await sftp.rename(oldPath, newPath);
const executionData = this.helpers.constructExecutionMetaData([{ json: { success: true } }], { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
if (operation === 'download') {
const path = this.getNodeParameter('path', i);
const options = this.getNodeParameter('options', i);
const binaryFile = await (0, tmp_promise_1.file)({ prefix: 'n8n-sftp-' });
try {
if (!options.enableConcurrentReads) {
await sftp.get(path, (0, fs_1.createWriteStream)(binaryFile.path));
}
else {
await sftp.fastGet(path, binaryFile.path, {
concurrency: options.maxConcurrentReads === undefined
? 5
: Number(options.maxConcurrentReads),
chunkSize: (options.chunkSize === undefined ? 64 : Number(options.chunkSize)) * 1024,
});
}
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i);
const remoteFilePath = this.getNodeParameter('path', i);
items[i].binary[dataPropertyNameDownload] = await this.nodeHelpers.copyBinaryFile(binaryFile.path, (0, path_1.basename)(remoteFilePath));
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(items[i]), { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
finally {
await binaryFile.cleanup();
}
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i);
await recursivelyCreateSftpDirs(sftp, remotePath);
if (this.getNodeParameter('binaryData', i)) {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
let uploadData;
if (binaryData.id) {
uploadData = await this.helpers.getBinaryStream(binaryData.id);
}
else {
uploadData = Buffer.from(binaryData.data, n8n_workflow_1.BINARY_ENCODING);
}
await sftp.put(uploadData, remotePath);
}
else {
// Is text file
const buffer = Buffer.from(this.getNodeParameter('fileContent', i), 'utf8');
await sftp.put(buffer, remotePath);
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(items[i]), { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
}
if (protocol === 'ftp') {
if (operation === 'list') {
const path = this.getNodeParameter('path', i);
const recursive = this.getNodeParameter('recursive', i);
let responseData;
if (recursive) {
responseData = await callRecursiveList(path, ftp, normalizeFtpItem);
}
else {
responseData = await ftp.list(path);
responseData.forEach((item) => normalizeFtpItem(item, path));
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
if (operation === 'delete') {
const path = this.getNodeParameter('path', i);
const options = this.getNodeParameter('options', i);
if (options.folder === true) {
await ftp.rmdir(path, !!options.recursive);
}
else {
await ftp.delete(path);
}
const executionData = this.helpers.constructExecutionMetaData([{ json: { success: true } }], { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
if (operation === 'download') {
const path = this.getNodeParameter('path', i);
const binaryFile = await (0, tmp_promise_1.file)({ prefix: 'n8n-sftp-' });
try {
const stream = await ftp.get(path);
await (0, promises_1.pipeline)(stream, (0, fs_1.createWriteStream)(binaryFile.path));
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i);
const remoteFilePath = this.getNodeParameter('path', i);
items[i].binary[dataPropertyNameDownload] = await this.nodeHelpers.copyBinaryFile(binaryFile.path, (0, path_1.basename)(remoteFilePath));
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(items[i]), { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
finally {
await binaryFile.cleanup();
}
}
if (operation === 'rename') {
const oldPath = this.getNodeParameter('oldPath', i);
const newPath = this.getNodeParameter('newPath', i);
const options = this.getNodeParameter('options', i);
try {
await ftp.rename(oldPath, newPath);
}
catch (error) {
if ([451, 550].includes(error.code) && options.createDirectories) {
const dirPath = newPath.replace((0, path_1.basename)(newPath), '');
await ftp.mkdir(dirPath, true);
await ftp.rename(oldPath, newPath);
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
}
}
const executionData = this.helpers.constructExecutionMetaData([{ json: { success: true } }], { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i);
const fileName = (0, path_1.basename)(remotePath);
const dirPath = remotePath.replace(fileName, '');
if (this.getNodeParameter('binaryData', i)) {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
let uploadData;
if (binaryData.id) {
uploadData = await this.helpers.getBinaryStream(binaryData.id);
}
else {
uploadData = Buffer.from(binaryData.data, n8n_workflow_1.BINARY_ENCODING);
}
try {
await ftp.put(uploadData, remotePath);
}
catch (error) {
if (error.code === 553) {
// Create directory
await ftp.mkdir(dirPath, true);
await ftp.put(uploadData, remotePath);
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
}
}
}
else {
// Is text file
const buffer = Buffer.from(this.getNodeParameter('fileContent', i), 'utf8');
try {
await ftp.put(buffer, remotePath);
}
catch (error) {
if (error.code === 553) {
// Create directory
await ftp.mkdir(dirPath, true);
await ftp.put(buffer, remotePath);
}
else {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
}
}
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(items[i]), { itemData: { item: i } });
returnItems = returnItems.concat(executionData);
}
}
}
catch (error) {
if (this.continueOnFail()) {
returnItems.push({ json: { error: error.message }, pairedItem: { item: i } });
continue;
}
throw error;
}
}
if (protocol === 'sftp') {
await sftp.end();
}
else {
await ftp.end();
}
}
catch (error) {
if (protocol === 'sftp') {
await sftp.end();
}
else {
await ftp.end();
}
throw error;
}
return [returnItems];
}
}
exports.Ftp = Ftp;
//# sourceMappingURL=Ftp.node.js.map