n8n/packages/nodes-base/nodes/Ftp.node.ts
Omar Ajoue 7ce7285f7a
Load credentials from the database (#1741)
* Changes to types so that credentials can be always loaded from DB

This first commit changes all return types from the execute functions
and calls to get credentials to be async so we can use await.

This is a first step as previously credentials were loaded in memory and
always available. We will now be loading them from the DB which requires
turning the whole call chain async.

* Fix updated files

* Removed unnecessary credential loading to improve performance

* Fix typo

*  Fix issue

* Updated new nodes to load credentials async

*  Remove not needed comment

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2021-08-20 18:57:30 +02:00

700 lines
17 KiB
TypeScript

import {
BINARY_ENCODING,
IExecuteFunctions
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
basename,
dirname,
} from 'path';
import * as ftpClient from 'promise-ftp';
import * as sftpClient from 'ssh2-sftp-client';
interface ReturnFtpItem {
type: string;
name: string;
size: number;
accessTime: Date;
modifyTime: Date;
rights: {
user: string;
group: string;
other: string;
};
owner: string | number;
group: string | number;
target: string;
sticky?: boolean;
path: string;
}
export class Ftp implements INodeType {
description: INodeTypeDescription = {
displayName: 'FTP',
name: 'ftp',
icon: 'fa:server',
group: ['input'],
version: 1,
subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}',
description: 'Transfers files via FTP or SFTP',
defaults: {
name: 'FTP',
color: '#303050',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'ftp',
required: true,
displayOptions: {
show: {
protocol: [
'ftp',
],
},
},
},
{
name: 'sftp',
required: true,
displayOptions: {
show: {
protocol: [
'sftp',
],
},
},
},
],
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.',
},
{
name: 'Download',
value: 'download',
description: 'Download a file.',
},
{
name: 'List',
value: 'list',
description: 'List folder content.',
},
{
name: 'Rename',
value: 'rename',
description: 'Rename/move oldPath to newPath.',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload a file.',
},
],
default: 'download',
description: 'Operation to perform.',
},
// ----------------------------------
// 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.',
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: 'When set to true, folders can be deleted.',
required: true,
},
{
displayName: 'Recursive',
displayOptions: {
show: {
folder: [
true,
],
},
},
name: 'recursive',
type: 'boolean',
default: false,
description: 'If true, remove all files and directories in target directory.',
required: true,
},
],
},
// ----------------------------------
// download
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: [
'download',
],
},
},
name: 'path',
type: 'string',
default: '',
placeholder: '/documents/invoice.txt',
description: 'The file path of the file to download. Has to contain the full path.',
required: true,
},
{
displayName: 'Binary Property',
displayOptions: {
show: {
operation: [
'download',
],
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
description: 'Object property name which holds binary data.',
required: true,
},
// ----------------------------------
// rename
// ----------------------------------
{
displayName: 'Old Path',
displayOptions: {
show: {
operation: [
'rename',
],
},
},
name: 'oldPath',
type: 'string',
default: '',
description: 'The old path',
required: true,
},
{
displayName: 'New Path',
displayOptions: {
show: {
operation: [
'rename',
],
},
},
name: 'newPath',
type: 'string',
default: '',
description: 'The new path',
required: true,
},
// ----------------------------------
// 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.',
required: true,
},
{
displayName: 'Binary Data',
displayOptions: {
show: {
operation: [
'upload',
],
},
},
name: 'binaryData',
type: 'boolean',
default: true,
description: 'The text content of the file to upload.',
},
{
displayName: 'Binary Property',
displayOptions: {
show: {
operation: [
'upload',
],
binaryData: [
true,
],
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
description: 'Object property name which holds binary data.',
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.',
},
// ----------------------------------
// list
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: [
'list',
],
},
},
name: 'path',
type: 'string',
default: '/',
description: 'Path of directory to list contents of.',
required: true,
},
{
displayName: 'Recursive',
displayOptions: {
show: {
operation: [
'list',
],
},
},
name: 'recursive',
type: 'boolean',
default: false,
description: 'Return object representing all directories / objects recursively found within SFTP server',
required: true,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
// const returnData: IDataObject[] = [];
const returnItems: INodeExecutionData[] = [];
const qs: IDataObject = {};
let responseData;
const operation = this.getNodeParameter('operation', 0) as string;
let credentials: ICredentialDataDecryptedObject | undefined = undefined;
const protocol = this.getNodeParameter('protocol', 0) as string;
if (protocol === 'sftp') {
credentials = await this.getCredentials('sftp');
} else {
credentials = await this.getCredentials('ftp');
}
try {
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'Failed to get credentials!');
}
let ftp : ftpClient;
let sftp : sftpClient;
if (protocol === 'sftp') {
sftp = new sftpClient();
await sftp.connect({
host: credentials.host as string,
port: credentials.port as number,
username: credentials.username as string,
password: credentials.password as string,
privateKey: credentials.privateKey as string | undefined,
passphrase: credentials.passphrase as string | undefined,
});
} else {
ftp = new ftpClient();
await ftp.connect({
host: credentials.host as string,
port: credentials.port as number,
user: credentials.username as string,
password: credentials.password as string,
});
}
for (let i = 0; i < items.length; i++) {
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (items[i].binary !== undefined) {
// 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) as string;
const recursive = this.getNodeParameter('recursive', i) as boolean;
if (recursive) {
responseData = await callRecursiveList(path, sftp!, normalizeSFtpItem);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
} else {
responseData = await sftp!.list(path);
responseData.forEach(item => normalizeSFtpItem(item as sftpClient.FileInfo, path));
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
}
}
if (operation === 'delete') {
const path = this.getNodeParameter('path', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.folder === true) {
responseData = await sftp!.rmdir(path, !!options.recursive);
} else {
responseData = await sftp!.delete(path);
}
returnItems.push({ json: { success: true } });
}
if (operation === 'rename') {
const oldPath = this.getNodeParameter('oldPath', i) as string;
const newPath = this.getNodeParameter('newPath', i) as string;
responseData = await sftp!.rename(oldPath, newPath);
returnItems.push({ json: { success: true } });
}
if (operation === 'download') {
const path = this.getNodeParameter('path', i) as string;
responseData = await sftp!.get(path);
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const filePathDownload = this.getNodeParameter('path', i) as string;
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData as Buffer, filePathDownload);
returnItems.push(items[i]);
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i) as string;
// Check if dir path exists
const dirPath = dirname(remotePath);
const dirExists = await sftp!.exists(dirPath);
// If dir does not exist, create all recursively in path
if (!dirExists) {
// Create directory
await sftp!.mkdir(dirPath, true);
}
if (this.getNodeParameter('binaryData', i) === true) {
// Is binary file to upload
const item = items[i];
if (item.binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[propertyNameUpload] === undefined) {
throw new NodeOperationError(this.getNode(), `No binary data property "${propertyNameUpload}" does not exists on item!`);
}
const buffer = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING) as Buffer;
await sftp!.put(buffer, remotePath);
} else {
// Is text file
const buffer = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8') as Buffer;
await sftp!.put(buffer, remotePath);
}
returnItems.push(items[i]);
}
}
if (protocol === 'ftp') {
if (operation === 'list') {
const path = this.getNodeParameter('path', i) as string;
const recursive = this.getNodeParameter('recursive', i) as boolean;
if (recursive) {
responseData = await callRecursiveList(path, ftp!, normalizeFtpItem);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
} else {
responseData = await ftp!.list(path);
responseData.forEach(item => normalizeFtpItem(item as ftpClient.ListingElement, path));
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
}
}
if (operation === 'delete') {
const path = this.getNodeParameter('path', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.folder === true) {
responseData = await ftp!.rmdir(path, !!options.recursive);
} else {
responseData = await ftp!.delete(path);
}
returnItems.push({ json: { success: true } });
}
if (operation === 'download') {
const path = this.getNodeParameter('path', i) as string;
responseData = await ftp!.get(path);
// Convert readable stream to buffer so that can be displayed properly
const chunks = [];
for await (const chunk of responseData) {
chunks.push(chunk);
}
// @ts-ignore
responseData = Buffer.concat(chunks);
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const filePathDownload = this.getNodeParameter('path', i) as string;
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData, filePathDownload);
returnItems.push(items[i]);
}
if (operation === 'rename') {
const oldPath = this.getNodeParameter('oldPath', i) as string;
const newPath = this.getNodeParameter('newPath', i) as string;
responseData = await ftp!.rename(oldPath, newPath);
returnItems.push({ json: { success: true } });
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i) as string;
const fileName = basename(remotePath);
const dirPath = remotePath.replace(fileName, '');
if (this.getNodeParameter('binaryData', i) === true) {
// Is binary file to upload
const item = items[i];
if (item.binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[propertyNameUpload] === undefined) {
throw new NodeOperationError(this.getNode(), `No binary data property "${propertyNameUpload}" does not exists on item!`);
}
const buffer = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING) as Buffer;
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 NodeApiError(this.getNode(), error);
}
}
} else {
// Is text file
const buffer = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8') as Buffer;
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 NodeApiError(this.getNode(), error);
}
}
}
returnItems.push(items[i]);
}
}
}
if (protocol === 'sftp') {
await sftp!.end();
} else {
await ftp!.end();
}
} catch (error) {
if (this.continueOnFail()) {
return this.prepareOutputData([{json:{ error: error.message }}]);
}
throw error;
}
return [returnItems];
}
}
function normalizeFtpItem(input: ftpClient.ListingElement, path: string) {
const item = input as unknown as ReturnFtpItem;
item.modifyTime = input.date;
item.path = `${path}${path.endsWith('/') ? '' : '/'}${item.name}`;
// @ts-ignore
item.date = undefined;
}
function normalizeSFtpItem(input: sftpClient.FileInfo, path: string) {
const item = input as unknown as ReturnFtpItem;
item.accessTime = new Date(input.accessTime);
item.modifyTime = new Date(input.modifyTime);
item.path = `${path}${path.endsWith('/') ? '' : '/'}${item.name}`;
}
async function callRecursiveList(path: string, client: sftpClient | ftpClient, normalizeFunction: (input: ftpClient.ListingElement & sftpClient.FileInfo, path: string) => void) {
const pathArray : string[] = [path];
let currentPath = path;
const directoryItems : sftpClient.FileInfo[] = [];
let index = 0;
do {
// tslint:disable-next-line: array-type
const returnData : sftpClient.FileInfo[] | (string | ftpClient.ListingElement)[] = await client.list(pathArray[index]);
// @ts-ignore
returnData.map((item : sftpClient.FileInfo) => {
if ((pathArray[index] as string).endsWith('/')) {
currentPath = `${pathArray[index]}${item.name}`;
} else {
currentPath = `${pathArray[index]}/${item.name}`;
}
// Is directory
if (item.type === 'd') {
pathArray.push(currentPath);
}
normalizeFunction(item as ftpClient.ListingElement & sftpClient.FileInfo, currentPath);
directoryItems.push(item);
});
index++;
} while (index <= pathArray.length - 1);
return directoryItems;
}