n8n/packages/nodes-base/nodes/NextCloud/NextCloud.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

1165 lines
27 KiB
TypeScript

import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
parseString,
} from 'xml2js';
import {
nextCloudApiRequest,
} from './GenericFunctions';
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: '#1cafff',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'nextCloudApi',
required: true,
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'nextCloudOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'The resource to operate on.',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'File',
value: 'file',
},
{
name: 'Folder',
value: 'folder',
},
{
name: 'User',
value: 'user',
},
],
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 contents of a given folder',
},
{
name: 'Move',
value: 'move',
description: 'Move a folder',
},
],
default: 'create',
description: 'The operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Invite a user to a NextCloud organization',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a user.',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve information about a single user.',
},
{
name: 'Get All',
value: 'getAll',
description: 'Retrieve a list of users.',
},
{
name: 'Update',
value: 'update',
description: 'Edit attributes related to a user.',
},
],
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. The path should start with "/"',
},
{
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. The path should start with "/"',
},
// ----------------------------------
// 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. The path should start with "/"',
},
// ----------------------------------
// 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. The path should start with "/"',
},
{
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. The path should start with "/"',
},
// ----------------------------------
// 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. The path should start with "/"',
},
{
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<br />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 absolute 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: [
false,
],
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<br />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. The path should start with "/"',
},
// ----------------------------------
// 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. The path should start with "/"',
},
// ----------------------------------
// user
// ----------------------------------
// ----------------------------------
// user:create
// ----------------------------------
{
displayName: 'Username',
name: 'userId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
placeholder: 'john',
description: 'Username the user will have.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
placeholder: 'john@email.com',
description: 'The email of the user to invite.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Display name',
name: 'displayName',
type: 'string',
default: '',
description: 'The display name of the user to invite.',
},
],
},
// ----------------------------------
// user:get/delete/update
// ----------------------------------
{
displayName: 'Username',
name: 'userId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'delete',
'get',
'update',
],
},
},
placeholder: 'john',
description: 'Username the user will have.',
},
// ----------------------------------
// user:getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Search',
name: 'search',
type: 'string',
default: '',
description: 'Optional search string.',
},
{
displayName: 'Offset',
name: 'offset',
type: 'number',
default: '',
description: 'Optional offset value.',
},
],
},
// ----------------------------------
// user:update
// ----------------------------------
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Fields',
name: 'field',
values: [
{
displayName: 'Key',
name: 'key',
type: 'options',
default: 'email',
options:
[
{
name: 'Address',
value: 'address',
description: 'The new address for the user.',
},
{
name: 'Display Name',
value: 'displayname',
description: 'The new display name for the user.',
},
{
name: 'Email',
value: 'email',
description: 'The new email for the user.',
},
{
name: 'Password',
value: 'password',
description: 'The new password for the user.',
},
{
name: 'Twitter',
value: 'twitter',
description: 'The new twitter handle for the user.',
},
{
name: 'Website',
value: 'website',
description: 'The new website for the user.',
},
],
description: 'Key of the updated attribute.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the updated attribute.',
},
],
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData().slice();
const returnData: IDataObject[] = [];
const authenticationMethod = this.getNodeParameter('authentication', 0);
let credentials;
if (authenticationMethod === 'accessToken') {
credentials = await this.getCredentials('nextCloudApi');
} else {
credentials = await this.getCredentials('nextCloudOAuth2Api');
}
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), '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 responseData: any; // tslint:disable-line:no-any
let body: string | Buffer | IDataObject = '';
const headers: IDataObject = {};
let qs;
for (let i = 0; i < items.length; i++) {
try {
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 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!`);
}
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 if (resource === 'user') {
if (operation === 'create') {
// ----------------------------------
// user:create
// ----------------------------------
requestMethod = 'POST';
endpoint = 'ocs/v1.php/cloud/users';
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
const userid = this.getNodeParameter('userId', i) as string;
const email = this.getNodeParameter('email', i) as string;
body = `userid=${userid}&email=${email}`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.displayName) {
body += `&displayName=${additionalFields.displayName}`;
}
}
if (operation === 'delete') {
// ----------------------------------
// user:delete
// ----------------------------------
requestMethod = 'DELETE';
const userid = this.getNodeParameter('userId', i) as string;
endpoint = `ocs/v1.php/cloud/users/${userid}`;
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (operation === 'get') {
// ----------------------------------
// user:get
// ----------------------------------
requestMethod = 'GET';
const userid = this.getNodeParameter('userId', i) as string;
endpoint = `ocs/v1.php/cloud/users/${userid}`;
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (operation === 'getAll') {
// ----------------------------------
// user:getAll
// ----------------------------------
requestMethod = 'GET';
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
qs = this.getNodeParameter('options', i) as IDataObject;
if (!returnAll) {
qs.limit = this.getNodeParameter('limit', i) as number;
}
endpoint = `ocs/v1.php/cloud/users`;
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (operation === 'update') {
// ----------------------------------
// user:update
// ----------------------------------
requestMethod = 'PUT';
const userid = this.getNodeParameter('userId', i) as string;
endpoint = `ocs/v1.php/cloud/users/${userid}`;
body = Object.entries((this.getNodeParameter('updateFields', i) as IDataObject).field as IDataObject).map(entry => {
const [key, value] = entry;
return `${key}=${value}`;
}).join('&');
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
} else {
throw new NodeOperationError(this.getNode(), `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);
}
let encoding = undefined;
if (resource === 'file' && operation === 'download') {
// Return the data as a buffer
encoding = null;
}
try {
responseData = await nextCloudApiRequest.call(this, requestMethod, endpoint, body, headers, encoding, qs);
} catch (error) {
if (this.continueOnFail()) {
if (resource === 'file' && operation === 'download') {
items[i].json = { error: error.message };
} else {
returnData.push({ error: error.message });
}
continue;
}
throw error;
}
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 === 'user') {
if (operation !== 'getAll') {
const jsonResponseData: IDataObject = await new Promise((resolve, reject) => {
parseString(responseData, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
if (data.ocs.meta.status !== 'ok') {
return reject(new Error(data.ocs.meta.message || data.ocs.meta.status));
}
if (operation === 'delete' || operation === 'update') {
resolve(data.ocs.meta as IDataObject);
} else {
resolve(data.ocs.data as IDataObject);
}
});
});
returnData.push(jsonResponseData as IDataObject);
} else {
const jsonResponseData: IDataObject[] = await new Promise((resolve, reject) => {
parseString(responseData, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
if (data.ocs.meta.status !== 'ok') {
return reject(new Error(data.ocs.meta.message));
}
if (typeof (data.ocs.data.users.element) === 'string') {
resolve([data.ocs.data.users.element] as IDataObject[]);
} else {
resolve(data.ocs.data.users.element as IDataObject[]);
}
});
});
jsonResponseData.forEach(value => {
returnData.push({ id: value } as IDataObject);
});
}
} 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
if (Array.isArray(jsonResponseData['d:multistatus']['d:response'])) {
// @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);
}
} catch (error) {
if (this.continueOnFail()) {
if (resource === 'file' && operation === 'download') {
items[i].json = { error: error.message };
} else {
returnData.push({ error: error.message });
}
continue;
}
throw error;
}
}
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)];
}
}
}