diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md
index 44eddb69a4..f3eca82a12 100644
--- a/packages/cli/BREAKING-CHANGES.md
+++ b/packages/cli/BREAKING-CHANGES.md
@@ -2,6 +2,18 @@
This list shows all the versions which include breaking changes and how to upgrade.
+## 0.111.0
+
+### What changed?
+In the Dropbox node, now all operations are performed relative to the user's root directory.
+
+### When is action necessary?
+If you are using the `folder:list` operation with the parameter `Folder Path` empty (root path) and have a Team Space in your Dropbox account.
+
+### How to upgrade:
+Open the Dropbox node, go to the `folder:list` operation, and make sure your logic is taking into account the team folders in the response.
+
+
## 0.105.0
### What changed?
diff --git a/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts b/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts
index c3c9f248db..81c84d92aa 100644
--- a/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts
+++ b/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts
@@ -7,6 +7,7 @@ const scopes = [
'files.content.write',
'files.content.read',
'sharing.read',
+ 'account_info.read',
];
export class DropboxOAuth2Api implements ICredentialType {
@@ -41,7 +42,7 @@ export class DropboxOAuth2Api implements ICredentialType {
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
- default: 'token_access_type=offline',
+ default: 'token_access_type=offline&force_reapprove=true',
},
{
displayName: 'Authentication',
diff --git a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts
index 790b181261..5f4e219652 100644
--- a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts
+++ b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts
@@ -11,21 +11,24 @@ import {
} from 'n8n-workflow';
import {
- dropboxApiRequest
+ dropboxApiRequest,
+ dropboxpiRequestAllItems,
+ getRootDirectory,
+ simplify,
} from './GenericFunctions';
export class Dropbox implements INodeType {
description: INodeTypeDescription = {
displayName: 'Dropbox',
name: 'dropbox',
- icon: 'file:dropbox.png',
+ icon: 'file:dropbox.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Access data on Dropbox',
defaults: {
name: 'Dropbox',
- color: '#0062ff',
+ color: '#007ee5',
},
inputs: ['main'],
outputs: ['main'],
@@ -84,6 +87,10 @@ export class Dropbox implements INodeType {
name: 'Folder',
value: 'folder',
},
+ {
+ name: 'Search',
+ value: 'search',
+ },
],
default: 'file',
description: 'The resource to operate on.',
@@ -176,6 +183,27 @@ export class Dropbox implements INodeType {
description: 'The operation to perform.',
},
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: [
+ 'search',
+ ],
+ },
+ },
+ options: [
+ {
+ name: 'Query',
+ value: 'query',
+ },
+ ],
+ default: 'query',
+ description: 'The operation to perform.',
+ },
+
// ----------------------------------
// file
// ----------------------------------
@@ -419,7 +447,189 @@ export class Dropbox implements INodeType {
description: 'Name of the binary property which contains
the data for the file to be uploaded.',
},
-
+ // ----------------------------------
+ // search:query
+ // ----------------------------------
+ {
+ displayName: 'Query',
+ name: 'query',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ operation: [
+ 'query',
+ ],
+ resource: [
+ 'search',
+ ],
+ },
+ },
+ description: ' The string to search for. May match across multiple fields based on the request arguments.',
+ },
+ {
+ displayName: 'File Status',
+ name: 'fileStatus',
+ type: 'options',
+ options: [
+ {
+ name: 'Active',
+ value: 'active',
+ },
+ {
+ name: 'Deleted',
+ value: 'deleted',
+ },
+ ],
+ default: 'active',
+ displayOptions: {
+ show: {
+ operation: [
+ 'query',
+ ],
+ resource: [
+ 'search',
+ ],
+ },
+ },
+ description: ' The string to search for. May match across multiple fields based on the request arguments.',
+ },
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ operation: [
+ 'query',
+ ],
+ resource: [
+ 'search',
+ ],
+ },
+ },
+ 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: [
+ 'search',
+ ],
+ operation: [
+ 'query',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ default: 100,
+ description: 'How many results to return.',
+ },
+ {
+ displayName: 'Simple',
+ name: 'simple',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ operation: [
+ 'query',
+ ],
+ resource: [
+ 'search',
+ ],
+ },
+ },
+ default: true,
+ description: 'When set to true a simplify version of the response will be used else the raw data.',
+ },
+ {
+ displayName: 'Filters',
+ name: 'filters',
+ type: 'collection',
+ placeholder: 'Add Filter',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: [
+ 'search',
+ ],
+ operation: [
+ 'query',
+ ],
+ },
+ },
+ options: [
+ {
+ displayName: 'File Categories',
+ name: 'file_categories',
+ type: 'multiOptions',
+ options: [
+ {
+ name: 'Audio (mp3, wav, mid, etc.)',
+ value: 'audio',
+ },
+ {
+ name: 'Document (doc, docx, txt, etc.)',
+ value: 'document',
+ },
+ {
+ name: 'Folder',
+ value: 'folder',
+ },
+ {
+ name: 'Image (jpg, png, gif, etc.)',
+ value: 'image',
+ },
+ {
+ name: 'Other',
+ value: 'other',
+ },
+ {
+ name: 'Dropbox Paper',
+ value: 'paper',
+ },
+ {
+ name: 'PDF',
+ value: 'pdf',
+ },
+ {
+ name: 'Presentation (ppt, pptx, key, etc.)',
+ value: 'presentation',
+ },
+ {
+ name: 'Spreadsheet (xlsx, xls, csv, etc.)',
+ value: 'spreadsheet',
+ },
+ {
+ name: 'Video (avi, wmv, mp4, etc.)',
+ value: 'video',
+ },
+ ],
+ default: [],
+ },
+ {
+ displayName: 'File Extensions',
+ name: 'file_extensions',
+ type: 'string',
+ default: '',
+ description: 'Multiple can be set separated by comma. Example: jpg,pdf',
+ },
+ {
+ displayName: 'Folder',
+ name: 'path',
+ type: 'string',
+ default: '',
+ description: 'If this field is not specified, this module searches the entire Dropbox',
+ },
+ ],
+ },
// ----------------------------------
// folder
@@ -469,7 +679,97 @@ export class Dropbox implements INodeType {
placeholder: '/invoices/2019/',
description: 'The path of which to list the content.',
},
-
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ operation: [
+ 'list',
+ ],
+ resource: [
+ 'folder',
+ ],
+ },
+ },
+ 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: [
+ 'folder',
+ ],
+ operation: [
+ 'list',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ default: 100,
+ description: 'How many results to return.',
+ },
+ {
+ displayName: 'Filters',
+ name: 'filters',
+ type: 'collection',
+ placeholder: 'Add Filter',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: [
+ 'folder',
+ ],
+ operation: [
+ 'list',
+ ],
+ },
+ },
+ options: [
+ {
+ displayName: 'Include Deleted',
+ name: 'include_deleted',
+ type: 'boolean',
+ default: false,
+ description: 'If true, the results will include entries for files and folders that used to exist but were deleted. The default for this field is False.',
+ },
+ {
+ displayName: 'Include Shared Members ',
+ name: 'include_has_explicit_shared_members',
+ type: 'boolean',
+ default: false,
+ description: 'If true, the results will include a flag for each file indicating whether or not that file has any explicit members. The default for this field is False.',
+ },
+ {
+ displayName: 'Include Mounted Folders ',
+ name: 'include_mounted_folders',
+ type: 'boolean',
+ default: true,
+ description: 'If true, the results will include entries under mounted folders which includes app folder, shared folder and team folder. The default for this field is True.',
+ },
+ {
+ displayName: 'Include Non Downloadable Files ',
+ name: 'include_non_downloadable_files',
+ type: 'boolean',
+ default: true,
+ description: 'If true, include files that are not downloadable, i.e. Google Docs. The default for this field is True.',
+ },
+ {
+ displayName: 'Recursive',
+ name: 'recursive',
+ type: 'boolean',
+ default: false,
+ description: 'If true, the list folder operation will be applied recursively to all subfolders and the response will contain contents of all subfolders. The default for this field is False.',
+ },
+ ],
+ },
],
};
@@ -484,11 +784,24 @@ export class Dropbox implements INodeType {
let endpoint = '';
let requestMethod = '';
+ let returnAll = false;
+ let property = '';
let body: IDataObject | Buffer;
let options;
const query: IDataObject = {};
- const headers: IDataObject = {};
+ let headers: IDataObject = {};
+ let simple = false;
+
+ // get the root directory to set it as the default search folder
+ const { root_info: { root_namespace_id } } = await getRootDirectory.call(this);
+
+ headers = {
+ 'dropbox-api-path-root': JSON.stringify({
+ '.tag': 'root',
+ 'root': root_namespace_id,
+ }),
+ };
for (let i = 0; i < items.length; i++) {
body = {};
@@ -545,7 +858,6 @@ export class Dropbox implements INodeType {
body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8');
}
}
-
} else if (resource === 'folder') {
if (operation === 'create') {
// ----------------------------------
@@ -564,20 +876,65 @@ export class Dropbox implements INodeType {
// list
// ----------------------------------
+ returnAll = this.getNodeParameter('returnAll', 0) as boolean;
+
+ const filters = this.getNodeParameter('filters', i) as IDataObject;
+
+ property = 'entries';
+
requestMethod = 'POST';
body = {
path: this.getNodeParameter('path', i) as string,
- limit: 2000,
+ limit: 1000,
};
- // TODO: If more files than the max-amount exist it has to be possible to
- // also request them.
+ if (returnAll === false) {
+ const limit = this.getNodeParameter('limit', 0) as number;
+ body.limit = limit;
+ }
+
+ Object.assign(body, filters);
endpoint = 'https://api.dropboxapi.com/2/files/list_folder';
}
+ } else if (resource === 'search') {
+ if (operation === 'query') {
+ // ----------------------------------
+ // query
+ // ----------------------------------
+
+ returnAll = this.getNodeParameter('returnAll', 0) as boolean;
+
+ simple = this.getNodeParameter('simple', 0) as boolean;
+
+ const filters = this.getNodeParameter('filters', i) as IDataObject;
+
+ property = 'matches';
+
+ requestMethod = 'POST';
+ body = {
+ query: this.getNodeParameter('query', i) as string,
+ options: {
+ filename_only: true,
+ },
+ };
+
+ if (filters.file_extensions) {
+ filters.file_extensions = (filters.file_extensions as string).split(',');
+ }
+
+ Object.assign(body.options, filters);
+
+ if (returnAll === false) {
+ const limit = this.getNodeParameter('limit', i) as number;
+ Object.assign(body.options, { max_results: limit });
+ }
+
+ endpoint = 'https://api.dropboxapi.com/2/files/search_v2';
+ }
}
- if (['file', 'folder'].includes(resource)) {
+ if (['file', 'folder', 'search'].includes(resource)) {
if (operation === 'copy') {
// ----------------------------------
// copy
@@ -625,7 +982,13 @@ export class Dropbox implements INodeType {
options = { encoding: null };
}
- let responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options);
+ let responseData;
+
+ if (returnAll === true) {
+ responseData = await dropboxpiRequestAllItems.call(this, property, requestMethod, endpoint, body, query, headers);
+ } else {
+ responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options);
+ }
if (resource === 'file' && operation === 'upload') {
responseData = JSON.parse(responseData);
@@ -665,7 +1028,11 @@ export class Dropbox implements INodeType {
'content_hash': 'contentHash',
};
- for (const item of responseData.entries) {
+ if (returnAll === false) {
+ responseData = responseData.entries;
+ }
+
+ for (const item of responseData) {
const newItem: IDataObject = {};
// Get the props and save them under a proper name
@@ -677,8 +1044,14 @@ export class Dropbox implements INodeType {
returnData.push(newItem as IDataObject);
}
+ } else if (resource === 'search' && operation === 'query') {
+ if (returnAll === true) {
+ returnData.push.apply(returnData, (simple === true) ? simplify(responseData) : responseData);
+ } else {
+ returnData.push.apply(returnData, (simple === true) ? simplify(responseData[property]) : responseData[property]);
+ }
} else {
- returnData.push(responseData as IDataObject);
+ returnData.push(responseData);
}
}
diff --git a/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts b/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts
index 880ec6bc8c..66533c4c01 100644
--- a/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts
+++ b/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts
@@ -20,7 +20,7 @@ import {
* @param {object} body
* @returns {Promise}
*/
-export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers?: object, option: IDataObject = {}): Promise {// tslint:disable-line:no-any
+export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers: object = {}, option: IDataObject = {}): Promise {// tslint:disable-line:no-any
const options: OptionsWithUri = {
headers,
@@ -67,3 +67,51 @@ export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions
throw error;
}
}
+
+export async function dropboxpiRequestAllItems(this: IExecuteFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any
+
+ const resource = this.getNodeParameter('resource', 0) as string;
+
+ const returnData: IDataObject[] = [];
+
+ const paginationEndpoint: IDataObject = {
+ 'folder': 'https://api.dropboxapi.com/2/files/list_folder/continue',
+ 'search': 'https://api.dropboxapi.com/2/files/search/continue_v2',
+ };
+
+ let responseData;
+ do {
+ responseData = await dropboxApiRequest.call(this, method, endpoint, body, query, headers);
+ const cursor = responseData.cursor;
+ if (cursor !== undefined) {
+ endpoint = paginationEndpoint[resource] as string;
+ body = { cursor };
+ }
+ returnData.push.apply(returnData, responseData[propertyName]);
+ } while (
+ responseData.has_more !== false
+ );
+
+ return returnData;
+}
+
+export function getRootDirectory(this: IHookFunctions | IExecuteFunctions) {
+ return dropboxApiRequest.call(this, 'POST', 'https://api.dropboxapi.com/2/users/get_current_account', {});
+}
+
+export function simplify(data: IDataObject[]) {
+ const results = [];
+ for (const element of data) {
+ const { '.tag': key } = element?.metadata as IDataObject;
+ const metadata = (element?.metadata as IDataObject)[key as string] as IDataObject;
+ delete element.metadata;
+ Object.assign(element, metadata);
+ if ((element?.match_type as IDataObject)['.tag']) {
+ element.match_type = (element?.match_type as IDataObject)['.tag'] as string;
+ }
+ results.push(element);
+ }
+ return results;
+}
+
+
diff --git a/packages/nodes-base/nodes/Dropbox/dropbox.png b/packages/nodes-base/nodes/Dropbox/dropbox.png
deleted file mode 100644
index 6fe6d86751..0000000000
Binary files a/packages/nodes-base/nodes/Dropbox/dropbox.png and /dev/null differ
diff --git a/packages/nodes-base/nodes/Dropbox/dropbox.svg b/packages/nodes-base/nodes/Dropbox/dropbox.svg
new file mode 100644
index 0000000000..d6896df517
--- /dev/null
+++ b/packages/nodes-base/nodes/Dropbox/dropbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file