import { BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; import { IDataObject, INodeTypeDescription, INodeExecutionData, INodeType, } from 'n8n-workflow'; import { parseString } from 'xml2js'; import { OptionsWithUri } from 'request'; export class NextCloud implements INodeType { description: INodeTypeDescription = { displayName: 'NextCloud', name: 'nextCloud', icon: 'file:nextcloud.png', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Access data on NextCloud', defaults: { name: 'NextCloud', color: '#22BB44', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'nextCloudApi', required: true, } ], properties: [ { displayName: 'Resource', name: 'resource', type: 'options', options: [ { name: 'File', value: 'file', }, { name: 'Folder', value: 'folder', }, ], default: 'file', description: 'The resource to operate on.', }, // ---------------------------------- // operations // ---------------------------------- { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: [ 'file', ], }, }, options: [ { name: 'Copy', value: 'copy', description: 'Copy a file', }, { name: 'Delete', value: 'delete', description: 'Delete a file', }, { name: 'Download', value: 'download', description: 'Download a file', }, { name: 'Move', value: 'move', description: 'Move a file', }, { name: 'Upload', value: 'upload', description: 'Upload a file', }, ], default: 'upload', description: 'The operation to perform.', }, { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: [ 'folder', ], }, }, options: [ { name: 'Copy', value: 'copy', description: 'Copy a folder', }, { name: 'Create', value: 'create', description: 'Create a folder', }, { name: 'Delete', value: 'delete', description: 'Delete a folder', }, { name: 'List', value: 'list', description: 'Return the files and folders in a given folder', }, { name: 'Move', value: 'move', description: 'Move a folder', }, ], default: 'create', description: 'The operation to perform.', }, // ---------------------------------- // file // ---------------------------------- // ---------------------------------- // file/folder:copy // ---------------------------------- { displayName: 'From Path', name: 'path', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'copy' ], resource: [ 'file', 'folder', ], }, }, placeholder: '/invoices/original.txt', description: 'The path of file or folder to copy.', }, { displayName: 'To Path', name: 'toPath', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'copy' ], resource: [ 'file', 'folder', ], }, }, placeholder: '/invoices/copy.txt', description: 'The destination path of file or folder.', }, // ---------------------------------- // file/folder:delete // ---------------------------------- { displayName: 'Delete Path', name: 'path', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'delete' ], resource: [ 'file', 'folder', ], }, }, placeholder: 'invoices/2019/invoice_1.pdf', description: 'The path to delete. Can be a single file or a whole folder.', }, // ---------------------------------- // file/folder:move // ---------------------------------- { displayName: 'From Path', name: 'path', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'move' ], resource: [ 'file', 'folder', ], }, }, placeholder: '/invoices/old_name.txt', description: 'The path of file or folder to move.', }, { displayName: 'To Path', name: 'toPath', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'move' ], resource: [ 'file', 'folder', ], }, }, placeholder: '/invoices/new_name.txt', description: 'The new path of file or folder.', }, // ---------------------------------- // file:download // ---------------------------------- { displayName: 'File Path', name: 'path', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'download' ], resource: [ 'file', ], }, }, placeholder: 'invoices/2019/invoice_1.pdf', description: 'The file path of the file to download. Has to contain the full path.', }, { displayName: 'Binary Property', name: 'binaryPropertyName', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: [ 'download' ], resource: [ 'file', ], }, }, description: 'Name of the binary property to which to
write the data of the read file.', }, // ---------------------------------- // file:upload // ---------------------------------- { displayName: 'File Path', name: 'path', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'upload' ], resource: [ 'file', ], }, }, placeholder: 'invoices/2019/invoice_1.pdf', description: 'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', }, { displayName: 'Binary Data', name: 'binaryDataUpload', type: 'boolean', default: false, required: true, displayOptions: { show: { operation: [ 'upload' ], resource: [ 'file', ], }, }, description: '', }, { displayName: 'File Content', name: 'fileContent', type: 'string', default: '', displayOptions: { show: { binaryDataUpload: [ true ], operation: [ 'upload' ], resource: [ 'file', ], }, }, placeholder: '', description: 'The text content of the file to upload.', }, { displayName: 'Binary Property', name: 'binaryPropertyName', type: 'string', default: 'data', required: true, displayOptions: { show: { binaryDataUpload: [ true ], operation: [ 'upload' ], resource: [ 'file', ], }, }, placeholder: '', description: 'Name of the binary property which contains
the data for the file to be uploaded.', }, // ---------------------------------- // folder // ---------------------------------- // ---------------------------------- // folder:create // ---------------------------------- { displayName: 'Folder', name: 'path', type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'create' ], resource: [ 'folder', ], }, }, placeholder: 'invoices/2019', description: 'The folder to create. The parent folder has to exist.', }, // ---------------------------------- // folder:list // ---------------------------------- { displayName: 'Folder Path', name: 'path', type: 'string', default: '', displayOptions: { show: { operation: [ 'list' ], resource: [ 'folder', ], }, }, placeholder: 'invoices/2019/', description: 'The path of which to list the content.', }, ], }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData().slice(); const returnData: IDataObject[] = []; const credentials = this.getCredentials('nextCloudApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); } const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; let endpoint = ''; let requestMethod = ''; let body: string | Buffer = ''; const headers: IDataObject = {}; for (let i = 0; i < items.length; i++) { if (resource === 'file') { if (operation === 'download') { // ---------------------------------- // download // ---------------------------------- requestMethod = 'GET'; endpoint = this.getNodeParameter('path', i) as string; } else if (operation === 'upload') { // ---------------------------------- // upload // ---------------------------------- requestMethod = 'PUT'; endpoint = this.getNodeParameter('path', i) as string; if (this.getNodeParameter('binaryDataUpload', i) === true) { // Is binary file to upload const item = items[i]; if (item.binary === undefined) { throw new Error('No binary data exists on item!'); } const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string; if (item.binary[propertyNameUpload] === undefined) { throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`); } body = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING); } else { // Is text file body = this.getNodeParameter('fileContent', i) as string; } } } else if (resource === 'folder') { if (operation === 'create') { // ---------------------------------- // create // ---------------------------------- requestMethod = 'MKCOL'; endpoint = this.getNodeParameter('path', i) as string; } else if (operation === 'list') { // ---------------------------------- // list // ---------------------------------- requestMethod = 'PROPFIND'; endpoint = this.getNodeParameter('path', i) as string; } } if (['file', 'folder'].includes(resource)) { if (operation === 'copy') { // ---------------------------------- // copy // ---------------------------------- requestMethod = 'COPY'; endpoint = this.getNodeParameter('path', i) as string; const toPath = this.getNodeParameter('toPath', i) as string; headers.Destination = `${credentials.webDavUrl}/${encodeURI(toPath)}`; } else if (operation === 'delete') { // ---------------------------------- // delete // ---------------------------------- requestMethod = 'DELETE'; endpoint = this.getNodeParameter('path', i) as string; } else if (operation === 'move') { // ---------------------------------- // move // ---------------------------------- requestMethod = 'MOVE'; endpoint = this.getNodeParameter('path', i) as string; const toPath = this.getNodeParameter('toPath', i) as string; headers.Destination = `${credentials.webDavUrl}/${encodeURI(toPath)}`; } } else { throw new Error(`The resource "${resource}" is not known!`); } // Make sure that the webdav URL does never have a trailing slash because // one gets added always automatically let webDavUrl = credentials.webDavUrl as string; if (webDavUrl.slice(-1) === '/') { webDavUrl = webDavUrl.slice(0, -1); } const options: OptionsWithUri = { auth: { user: credentials.user as string, pass: credentials.password as string, }, headers, method: requestMethod, body, qs: {}, uri: `${credentials.webDavUrl}/${encodeURI(endpoint)}`, json: false, }; if (resource === 'file' && operation === 'download') { // Return the data as a buffer options.encoding = null; } const responseData = await this.helpers.request(options); if (resource === 'file' && operation === 'download') { 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; const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData, endpoint); } else if (resource === 'folder' && operation === 'list') { const jsonResponseData: IDataObject = await new Promise((resolve, reject) => { parseString(responseData, { explicitArray: false }, (err, data) => { if (err) { return reject(err); } resolve(data as IDataObject); }); }); const propNames: { [key: string]: string } = { 'd:getlastmodified': 'lastModified', 'd:getcontentlength': 'contentLength', 'd:getcontenttype': 'contentType', }; if (jsonResponseData['d:multistatus'] !== undefined && jsonResponseData['d:multistatus'] !== null && (jsonResponseData['d:multistatus'] as IDataObject)['d:response'] !== undefined && (jsonResponseData['d:multistatus'] as IDataObject)['d:response'] !== null) { let skippedFirst = false; // @ts-ignore for (const item of jsonResponseData['d:multistatus']['d:response']) { if (skippedFirst === false) { skippedFirst = true; continue; } const newItem: IDataObject = {}; newItem.path = item['d:href'].slice(19); const props = item['d:propstat'][0]['d:prop']; // Get the props and save them under a proper name for (const propName of Object.keys(propNames)) { if (props[propName] !== undefined) { newItem[propNames[propName]] = props[propName]; } } if (props['d:resourcetype'] === '') { newItem.type = 'file'; } else { newItem.type = 'folder'; } newItem.eTag = props['d:getetag'].slice(1, -1); returnData.push(newItem as IDataObject); } } } else { returnData.push(responseData as IDataObject); } } if (resource === 'file' && operation === 'download') { // For file downloads the files get attached to the existing items return this.prepareOutputData(items); } else { // For all other ones does the output get replaced return [this.helpers.returnJsonArray(returnData)]; } } }