first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { router } from './actions/router';
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { listSearch } from './methods';
|
||||
|
||||
export class GoogleDriveV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = { listSearch };
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
return await router.call(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { GOOGLE_DRIVE_FILE_URL_REGEX, GOOGLE_DRIVE_FOLDER_URL_REGEX } from '../../../constants';
|
||||
import { DRIVE, RLC_DRIVE_DEFAULT } from '../helpers/interfaces';
|
||||
|
||||
export const fileRLC: INodeProperties = {
|
||||
displayName: 'File',
|
||||
name: 'fileId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'File',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a file...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'fileSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Link',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
placeholder:
|
||||
'e.g. https://drive.google.com/file/d/1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A/edit',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FILE_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FILE_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive File URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. 1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
errorMessage: 'Not a valid Google Drive File ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
url: '=https://drive.google.com/file/d/{{$value}}/view',
|
||||
},
|
||||
],
|
||||
description: 'The file to operate on',
|
||||
};
|
||||
|
||||
export const folderNoRootRLC: INodeProperties = {
|
||||
displayName: 'Folder',
|
||||
name: 'folderNoRootId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a folder...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'folderSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Link',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://drive.google.com/drive/folders/1Tx9WHbA3wBpPB4C_HcoZDH9WZFWYxAMU',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive Folder URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. 1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
errorMessage: 'Not a valid Google Drive Folder ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
url: '=https://drive.google.com/drive/folders/{{$value}}',
|
||||
},
|
||||
],
|
||||
description: 'The folder to operate on',
|
||||
};
|
||||
|
||||
export const folderRLC: INodeProperties = {
|
||||
displayName: 'Folder',
|
||||
name: 'folderId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: 'root', cachedResultName: '/ (Root folder)' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a folder...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'folderSearchWithDefault',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Link',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://drive.google.com/drive/folders/1Tx9WHbA3wBpPB4C_HcoZDH9WZFWYxAMU',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive Folder URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. 1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
errorMessage: 'Not a valid Google Drive Folder ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
url: '=https://drive.google.com/drive/folders/{{$value}}',
|
||||
},
|
||||
],
|
||||
description: 'The folder to operate on',
|
||||
};
|
||||
|
||||
export const driveRLC: INodeProperties = {
|
||||
displayName: 'Drive',
|
||||
name: 'driveId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: RLC_DRIVE_DEFAULT },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'Drive',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a drive...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'driveSearchWithDefault',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Link',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
placeholder: 'https://drive.google.com/drive/folders/0AaaaaAAAAAAAaa',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive Drive URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
hint: 'The ID of the shared drive',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
errorMessage: 'Not a valid Google Drive Drive ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
url: '=https://drive.google.com/drive/folders/{{$value}}',
|
||||
},
|
||||
],
|
||||
description: 'The ID of the drive',
|
||||
};
|
||||
|
||||
export const sharedDriveRLC: INodeProperties = {
|
||||
displayName: 'Shared Drive',
|
||||
name: 'driveId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'Drive',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a shared drive...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'driveSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Link',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://drive.google.com/drive/u/1/folders/0AIjtcbwnjtcbwn9PVA',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive Drive URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
// hint: 'The ID of the shared drive',
|
||||
placeholder: 'e.g. 0AMXTKI5ZSiM7Uk9PVA',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
errorMessage: 'Not a valid Google Drive Drive ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
url: '=https://drive.google.com/drive/folders/{{$value}}',
|
||||
},
|
||||
],
|
||||
description: 'The shared drive to operate on',
|
||||
};
|
||||
|
||||
export const shareOptions: INodeProperties = {
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Email Message',
|
||||
name: 'emailMessage',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A plain text custom message to include in the notification email',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Move To New Owners Root',
|
||||
name: 'moveToNewOwnersRoot',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description:
|
||||
"<p>This parameter only takes effect if the item is not in a shared drive and the request is attempting to transfer the ownership of the item.</p><p>When set to true, the item is moved to the new owner's My Drive root folder and all prior parents removed.</p>",
|
||||
},
|
||||
{
|
||||
displayName: 'Send Notification Email',
|
||||
name: 'sendNotificationEmail',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to send a notification email when sharing to users or groups',
|
||||
},
|
||||
{
|
||||
displayName: 'Transfer Ownership',
|
||||
name: 'transferOwnership',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to transfer ownership to the specified user and downgrade the current owner to a writer',
|
||||
},
|
||||
{
|
||||
displayName: 'Use Domain Admin Access',
|
||||
name: 'useDomainAdminAccess',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to perform the operation as domain administrator, i.e. if you are an administrator of the domain to which the shared drive belongs, you will be granted access automatically.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const permissionsOptions: INodeProperties = {
|
||||
displayName: 'Permissions',
|
||||
name: 'permissionsUi',
|
||||
placeholder: 'Add Permission',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Permission',
|
||||
name: 'permissionsValues',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
description: 'Defines what users can do with the file or folder',
|
||||
options: [
|
||||
{
|
||||
name: 'Commenter',
|
||||
value: 'commenter',
|
||||
},
|
||||
{
|
||||
name: 'File Organizer',
|
||||
value: 'fileOrganizer',
|
||||
},
|
||||
{
|
||||
name: 'Organizer',
|
||||
value: 'organizer',
|
||||
},
|
||||
{
|
||||
name: 'Owner',
|
||||
value: 'owner',
|
||||
},
|
||||
{
|
||||
name: 'Reader',
|
||||
value: 'reader',
|
||||
},
|
||||
{
|
||||
name: 'Writer',
|
||||
value: 'writer',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
name: 'Group',
|
||||
value: 'group',
|
||||
},
|
||||
{
|
||||
name: 'Domain',
|
||||
value: 'domain',
|
||||
},
|
||||
{
|
||||
name: 'Anyone',
|
||||
value: 'anyone',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description:
|
||||
'The scope of the permission. A permission with type=user applies to a specific user whereas a permission with type=domain applies to everyone in a specific domain.',
|
||||
},
|
||||
{
|
||||
displayName: 'Email Address',
|
||||
name: 'emailAddress',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['user', 'group'],
|
||||
},
|
||||
},
|
||||
placeholder: '“e.g. name@mail.com',
|
||||
default: '',
|
||||
description: 'The email address of the user or group to which this permission refers',
|
||||
},
|
||||
{
|
||||
displayName: 'Domain',
|
||||
name: 'domain',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['domain'],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. mycompany.com',
|
||||
default: '',
|
||||
description: 'The domain to which this permission refers',
|
||||
},
|
||||
{
|
||||
displayName: 'Allow File Discovery',
|
||||
name: 'allowFileDiscovery',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['domain', 'anyone'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to allow the file to be discovered through search',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const updateCommonOptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'APP Properties',
|
||||
name: 'appPropertiesUi',
|
||||
placeholder: 'Add Property',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description:
|
||||
'A collection of arbitrary key-value pairs which are private to the requesting app',
|
||||
options: [
|
||||
{
|
||||
name: 'appPropertyValues',
|
||||
displayName: 'APP Property',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the key to add',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value to set for the key',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Properties',
|
||||
name: 'propertiesUi',
|
||||
placeholder: 'Add Property',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'A collection of arbitrary key-value pairs which are visible to all apps',
|
||||
options: [
|
||||
{
|
||||
name: 'propertyValues',
|
||||
displayName: 'Property',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the key to add',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value to set for the key',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Keep Revision Forever',
|
||||
name: 'keepRevisionForever',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
"Whether to set the 'keepForever' field in the new head revision. This is only applicable to files with binary content in Google Drive. Only 200 revisions for the file can be kept forever. If the limit is reached, try deleting pinned revisions.",
|
||||
},
|
||||
{
|
||||
displayName: 'OCR Language',
|
||||
name: 'ocrLanguage',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. en',
|
||||
description: 'A language hint for OCR processing during image import (ISO 639-1 code)',
|
||||
},
|
||||
{
|
||||
displayName: 'Use Content As Indexable Text',
|
||||
name: 'useContentAsIndexableText',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to use the uploaded content as indexable text',
|
||||
},
|
||||
];
|
||||
|
||||
export const fileTypesOptions = [
|
||||
{
|
||||
name: 'All',
|
||||
value: '*',
|
||||
description: 'Return all file types',
|
||||
},
|
||||
{
|
||||
name: '3rd Party Shortcut',
|
||||
value: DRIVE.SDK,
|
||||
},
|
||||
{
|
||||
name: 'Audio',
|
||||
value: DRIVE.AUDIO,
|
||||
},
|
||||
{
|
||||
name: 'Folder',
|
||||
value: DRIVE.FOLDER,
|
||||
},
|
||||
{
|
||||
name: 'Google Apps Scripts',
|
||||
value: DRIVE.APP_SCRIPTS,
|
||||
},
|
||||
{
|
||||
name: 'Google Docs',
|
||||
value: DRIVE.DOCUMENT,
|
||||
},
|
||||
{
|
||||
name: 'Google Drawing',
|
||||
value: DRIVE.DRAWING,
|
||||
},
|
||||
{
|
||||
name: 'Google Forms',
|
||||
value: DRIVE.FORM,
|
||||
},
|
||||
{
|
||||
name: 'Google Fusion Tables',
|
||||
value: DRIVE.FUSIONTABLE,
|
||||
},
|
||||
{
|
||||
name: 'Google My Maps',
|
||||
value: DRIVE.MAP,
|
||||
},
|
||||
{
|
||||
name: 'Google Sheets',
|
||||
value: DRIVE.SPREADSHEET,
|
||||
},
|
||||
{
|
||||
name: 'Google Sites',
|
||||
value: DRIVE.SITES,
|
||||
},
|
||||
{
|
||||
name: 'Google Slides',
|
||||
value: DRIVE.PRESENTATION,
|
||||
},
|
||||
{
|
||||
name: 'Photo',
|
||||
value: DRIVE.PHOTO,
|
||||
},
|
||||
{
|
||||
name: 'Unknown',
|
||||
value: DRIVE.UNKNOWN,
|
||||
},
|
||||
{
|
||||
name: 'Video',
|
||||
value: DRIVE.VIDEO,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as deleteDrive from './deleteDrive.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as list from './list.operation';
|
||||
import * as update from './update.operation';
|
||||
|
||||
export { create, deleteDrive, get, list, update };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a shared drive',
|
||||
action: 'Create shared drive',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'deleteDrive',
|
||||
description: 'Permanently delete a shared drive',
|
||||
action: 'Delete shared drive',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a shared drive',
|
||||
action: 'Get shared drive',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'list',
|
||||
description: 'Get the list of shared drives',
|
||||
action: 'Get many shared drives',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a shared drive',
|
||||
action: 'Update shared drive',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['drive'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...create.description,
|
||||
...deleteDrive.description,
|
||||
...get.description,
|
||||
...list.description,
|
||||
...update.description,
|
||||
];
|
||||
@@ -0,0 +1,267 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. New Shared Drive',
|
||||
description: 'The name of the shared drive to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Capabilities',
|
||||
name: 'capabilities',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Can Add Children',
|
||||
name: 'canAddChildren',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can add children to folders in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Change Copy Requires Writer Permission Restriction',
|
||||
name: 'canChangeCopyRequiresWriterPermissionRestriction',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can change the copyRequiresWriterPermission restriction of this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Change Domain Users Only Restriction',
|
||||
name: 'canChangeDomainUsersOnlyRestriction',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can change the domainUsersOnly restriction of this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Change Drive Background',
|
||||
name: 'canChangeDriveBackground',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can change the background of this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Change Drive Members Only Restriction',
|
||||
name: 'canChangeDriveMembersOnlyRestriction',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can change the driveMembersOnly restriction of this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Comment',
|
||||
name: 'canComment',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can comment on files in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Copy',
|
||||
name: 'canCopy',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can copy files in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Delete Children',
|
||||
name: 'canDeleteChildren',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can delete children from folders in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Delete Drive',
|
||||
name: 'canDeleteDrive',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can delete this shared drive. Attempting to delete the shared drive may still fail if there are untrashed items inside the shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Download',
|
||||
name: 'canDownload',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can download files in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Edit',
|
||||
name: 'canEdit',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can edit files in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can List Children',
|
||||
name: 'canListChildren',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can list the children of folders in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Manage Members',
|
||||
name: 'canManageMembers',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can add members to this shared drive or remove them or change their role',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Read Revisions',
|
||||
name: 'canReadRevisions',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can read the revisions resource of files in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Rename',
|
||||
name: 'canRename',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can rename files or folders in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Rename Drive',
|
||||
name: 'canRenameDrive',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can rename this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Share',
|
||||
name: 'canShare',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the current user can share files or folders in this shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Can Trash Children',
|
||||
name: 'canTrashChildren',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the current user can trash children from folders in this shared drive',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Color RGB',
|
||||
name: 'colorRgb',
|
||||
type: 'color',
|
||||
default: '',
|
||||
description: 'The color of this shared drive as an RGB hex string',
|
||||
},
|
||||
{
|
||||
displayName: 'Hidden',
|
||||
name: 'hidden',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the shared drive is hidden from default view',
|
||||
},
|
||||
{
|
||||
displayName: 'Restrictions',
|
||||
name: 'restrictions',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Admin Managed Restrictions',
|
||||
name: 'adminManagedRestrictions',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Copy Requires Writer Permission',
|
||||
name: 'copyRequiresWriterPermission',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Domain Users Only',
|
||||
name: 'domainUsersOnly',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether access to this shared drive and items inside this shared drive is restricted to users of the domain to which this shared drive belongs. This restriction may be overridden by other sharing policies controlled outside of this shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Drive Members Only',
|
||||
name: 'driveMembersOnly',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether access to items inside this shared drive is restricted to its members',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['drive'],
|
||||
operation: ['create'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const options = this.getNodeParameter('options', i);
|
||||
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
|
||||
const body: IDataObject = {
|
||||
name,
|
||||
};
|
||||
|
||||
Object.assign(body, options);
|
||||
|
||||
const response = await googleApiRequest.call(this, 'POST', '/drive/v3/drives', body, {
|
||||
requestId: uuid(),
|
||||
});
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { sharedDriveRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...sharedDriveRLC,
|
||||
description: 'The shared drive to delete',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['drive'],
|
||||
operation: ['deleteDrive'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
await googleApiRequest.call(this, 'DELETE', `/drive/v3/drives/${driveId}`);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ success: true }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { sharedDriveRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...sharedDriveRLC,
|
||||
description: 'The shared drive to get',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Use Domain Admin Access',
|
||||
name: 'useDomainAdminAccess',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to issue the request as a domain administrator; if set to true, then the requester will be granted access if they are an administrator of the domain to which the shared drive belongs',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['drive'],
|
||||
operation: ['get'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const options = this.getNodeParameter('options', i);
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const qs: IDataObject = {};
|
||||
|
||||
Object.assign(qs, options);
|
||||
|
||||
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/drives/${driveId}`, {}, qs);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 200,
|
||||
},
|
||||
default: 100,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'q',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Query string for searching shared drives. See the <a href="https://developers.google.com/drive/api/v3/search-shareddrives">"Search for shared drives"</a> guide for supported syntax.',
|
||||
},
|
||||
{
|
||||
displayName: 'Use Domain Admin Access',
|
||||
name: 'useDomainAdminAccess',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to issue the request as a domain administrator; if set to true, then the requester will be granted access if they are an administrator of the domain to which the shared drive belongs',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['drive'],
|
||||
operation: ['list'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const options = this.getNodeParameter('options', i);
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
|
||||
const qs: IDataObject = {};
|
||||
|
||||
let response: IDataObject[] = [];
|
||||
|
||||
Object.assign(qs, options);
|
||||
|
||||
if (returnAll) {
|
||||
response = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'GET',
|
||||
'drives',
|
||||
'/drive/v3/drives',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.pageSize = this.getNodeParameter('limit', i);
|
||||
const data = await googleApiRequest.call(this, 'GET', '/drive/v3/drives', {}, qs);
|
||||
response = data.drives as IDataObject[];
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { sharedDriveRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...sharedDriveRLC,
|
||||
description: 'The shared drive to update',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
resource: ['drive'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Color RGB',
|
||||
name: 'colorRgb',
|
||||
type: 'color',
|
||||
default: '',
|
||||
description: 'The color of this shared drive as an RGB hex string',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The updated name of the shared drive',
|
||||
},
|
||||
{
|
||||
displayName: 'Restrictions',
|
||||
name: 'restrictions',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Admin Managed Restrictions',
|
||||
name: 'adminManagedRestrictions',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Copy Requires Writer Permission',
|
||||
name: 'copyRequiresWriterPermission',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Domain Users Only',
|
||||
name: 'domainUsersOnly',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether access to this shared drive and items inside this shared drive is restricted to users of the domain to which this shared drive belongs. This restriction may be overridden by other sharing policies controlled outside of this shared drive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Drive Members Only',
|
||||
name: 'driveMembersOnly',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether access to items inside this shared drive is restricted to its members',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['drive'],
|
||||
operation: ['update'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
Object.assign(body, options);
|
||||
|
||||
const response = await googleApiRequest.call(this, 'PATCH', `/drive/v3/drives/${driveId}`, body);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as copy from './copy.operation';
|
||||
import * as createFromText from './createFromText.operation';
|
||||
import * as deleteFile from './deleteFile.operation';
|
||||
import * as download from './download.operation';
|
||||
import * as move from './move.operation';
|
||||
import * as share from './share.operation';
|
||||
import * as update from './update.operation';
|
||||
import * as upload from './upload.operation';
|
||||
|
||||
export { copy, createFromText, deleteFile, download, move, share, update, upload };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Copy',
|
||||
value: 'copy',
|
||||
description: 'Create a copy of an existing file',
|
||||
action: 'Copy file',
|
||||
},
|
||||
{
|
||||
name: 'Create From Text',
|
||||
value: 'createFromText',
|
||||
description: 'Create a file from a provided text',
|
||||
action: 'Create file from text',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'deleteFile',
|
||||
description: 'Permanently delete a file',
|
||||
action: 'Delete a file',
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
value: 'download',
|
||||
description: 'Download a file',
|
||||
action: 'Download file',
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
value: 'move',
|
||||
description: 'Move a file to another folder',
|
||||
action: 'Move file',
|
||||
},
|
||||
{
|
||||
name: 'Share',
|
||||
value: 'share',
|
||||
description: 'Add sharing permissions to a file',
|
||||
action: 'Share file',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a file',
|
||||
action: 'Update file',
|
||||
},
|
||||
{
|
||||
name: 'Upload',
|
||||
value: 'upload',
|
||||
description: 'Upload an existing file to Google Drive',
|
||||
action: 'Upload file',
|
||||
},
|
||||
],
|
||||
default: 'upload',
|
||||
},
|
||||
...copy.description,
|
||||
...deleteFile.description,
|
||||
...createFromText.description,
|
||||
...download.description,
|
||||
...move.description,
|
||||
...share.description,
|
||||
...update.description,
|
||||
...upload.description,
|
||||
];
|
||||
@@ -0,0 +1,137 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameterResourceLocator,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { setParentFolder } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...fileRLC,
|
||||
description: 'The file to copy',
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. My File',
|
||||
description:
|
||||
'The name of the new file. If not set, “Copy of {original file name}” will be used.',
|
||||
},
|
||||
{
|
||||
displayName: 'Copy In The Same Folder',
|
||||
name: 'sameFolder',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to copy the file in the same folder as the original file',
|
||||
},
|
||||
{
|
||||
...driveRLC,
|
||||
displayName: 'Parent Drive',
|
||||
description: 'The drive where to save the copied file',
|
||||
displayOptions: { show: { sameFolder: [false] } },
|
||||
},
|
||||
{
|
||||
...folderRLC,
|
||||
displayName: 'Parent Folder',
|
||||
description: 'The folder where to save the copied file',
|
||||
displayOptions: { show: { sameFolder: [false] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Copy Requires Writer Permission',
|
||||
name: 'copyRequiresWriterPermission',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the options to copy, print, or download this file, should be disabled for readers and commenters',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A short description of the file',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['copy'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const file = this.getNodeParameter('fileId', i) as INodeParameterResourceLocator;
|
||||
|
||||
const fileId = file.value;
|
||||
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
let name = this.getNodeParameter('name', i) as string;
|
||||
name = name ? name : `Copy of ${file.cachedResultName}`;
|
||||
|
||||
const copyRequiresWriterPermission = options.copyRequiresWriterPermission || false;
|
||||
|
||||
const qs = {
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
};
|
||||
|
||||
const parents: string[] = [];
|
||||
const sameFolder = this.getNodeParameter('sameFolder', i) as boolean;
|
||||
|
||||
if (!sameFolder) {
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const folderId = this.getNodeParameter('folderId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
parents.push(setParentFolder(folderId, driveId));
|
||||
}
|
||||
|
||||
const body: IDataObject = { copyRequiresWriterPermission, parents, name };
|
||||
|
||||
if (options.description) {
|
||||
body.description = options.description;
|
||||
}
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/drive/v3/files/${fileId}/copy`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { DRIVE } from '../../helpers/interfaces';
|
||||
import { setFileProperties, setParentFolder, setUpdateCommonParams } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
|
||||
|
||||
import FormData from 'form-data';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'File Content',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
description: 'The text to create the file with',
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. My New File',
|
||||
description:
|
||||
"The name of the file you want to create. If not specified, 'Untitled' will be used.",
|
||||
},
|
||||
{
|
||||
...driveRLC,
|
||||
displayName: 'Parent Drive',
|
||||
required: false,
|
||||
description: 'The drive where to create the new file',
|
||||
},
|
||||
{
|
||||
...folderRLC,
|
||||
displayName: 'Parent Folder',
|
||||
required: false,
|
||||
description: 'The folder where to create the new file',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
...updateCommonOptions,
|
||||
{
|
||||
displayName: 'Convert to Google Document',
|
||||
name: 'convertToGoogleDocument',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to create a Google Document (instead of the .txt default format)',
|
||||
hint: 'Google Docs API has to be enabled in the <a href="https://console.developers.google.com/apis/library/docs.googleapis.com" target="_blank">Google API Console</a>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['createFromText'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const name = (this.getNodeParameter('name', i) as string) || 'Untitled';
|
||||
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const convertToGoogleDocument = (options.convertToGoogleDocument as boolean) || false;
|
||||
const mimeType = convertToGoogleDocument ? DRIVE.DOCUMENT : 'text/plain';
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const folderId = this.getNodeParameter('folderId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const metadata = {
|
||||
name,
|
||||
parents: [setParentFolder(folderId, driveId)],
|
||||
mimeType,
|
||||
};
|
||||
|
||||
const bodyParameters = setFileProperties(metadata, options);
|
||||
|
||||
const qs = setUpdateCommonParams(
|
||||
{
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
let response;
|
||||
if (convertToGoogleDocument) {
|
||||
const document = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/drive/v3/files',
|
||||
bodyParameters,
|
||||
qs,
|
||||
);
|
||||
|
||||
const text = this.getNodeParameter('content', i, '') as string;
|
||||
|
||||
const body = {
|
||||
requests: [
|
||||
{
|
||||
insertText: {
|
||||
text,
|
||||
endOfSegmentLocation: {
|
||||
segmentId: '', //empty segment ID signifies the document's body
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const updateResponse = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'',
|
||||
body,
|
||||
undefined,
|
||||
`https://docs.googleapis.com/v1/documents/${document.id}:batchUpdate`,
|
||||
);
|
||||
|
||||
response = { id: updateResponse.documentId };
|
||||
} else {
|
||||
const content = Buffer.from(this.getNodeParameter('content', i, '') as string, 'utf8');
|
||||
const contentLength = content.byteLength;
|
||||
|
||||
const multiPartBody = new FormData();
|
||||
multiPartBody.append('metadata', JSON.stringify(metadata), {
|
||||
contentType: 'application/json',
|
||||
});
|
||||
multiPartBody.append('data', content, {
|
||||
contentType: mimeType,
|
||||
knownLength: contentLength,
|
||||
});
|
||||
|
||||
const uploadData = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
multiPartBody.getBuffer(),
|
||||
{
|
||||
uploadType: 'multipart',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': `multipart/related; boundary=${multiPartBody.getBoundary()}`,
|
||||
'Content-Length': multiPartBody.getLengthSync(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const uploadId = uploadData.id;
|
||||
|
||||
qs.addParents = setParentFolder(folderId, driveId);
|
||||
delete bodyParameters.parents;
|
||||
|
||||
const responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/drive/v3/files/${uploadId}`,
|
||||
bodyParameters,
|
||||
qs,
|
||||
);
|
||||
|
||||
response = { id: responseData.id };
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { fileRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...fileRLC,
|
||||
description: 'The file to delete',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Delete Permanently',
|
||||
name: 'deletePermanently',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to delete the file immediately. If false, the file will be moved to the trash.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['deleteFile'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const deletePermanently = this.getNodeParameter('options.deletePermanently', i, false) as boolean;
|
||||
|
||||
const qs = {
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if (deletePermanently) {
|
||||
await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${fileId}`, undefined, qs);
|
||||
} else {
|
||||
await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${fileId}`, { trashed: true }, qs);
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({
|
||||
id: fileId,
|
||||
success: true,
|
||||
}),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { fileRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...fileRLC,
|
||||
description: 'The file to download',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Put Output File in Field',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. data',
|
||||
default: 'data',
|
||||
description: 'Use this field name in the following nodes, to use the binary file data',
|
||||
hint: 'The name of the output binary field to put the file in',
|
||||
},
|
||||
{
|
||||
displayName: 'Google File Conversion',
|
||||
name: 'googleFileConversion',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add Conversion',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Conversion',
|
||||
name: 'conversion',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Google Docs',
|
||||
name: 'docsToFormat',
|
||||
type: 'options',
|
||||
|
||||
options: [
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'text/html',
|
||||
},
|
||||
{
|
||||
name: 'MS Word Document',
|
||||
value:
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
},
|
||||
{
|
||||
name: 'Open Office Document',
|
||||
value: 'application/vnd.oasis.opendocument.text',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
value: 'application/pdf',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Rich Text (rtf)',
|
||||
value: 'application/rtf',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Text (txt)',
|
||||
value: 'text/plain',
|
||||
},
|
||||
],
|
||||
default: 'text/html',
|
||||
description: 'Format used to export when downloading Google Docs files',
|
||||
},
|
||||
{
|
||||
displayName: 'Google Drawings',
|
||||
name: 'drawingsToFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'JPEG',
|
||||
value: 'image/jpeg',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
value: 'application/pdf',
|
||||
},
|
||||
{
|
||||
name: 'PNG',
|
||||
value: 'image/png',
|
||||
},
|
||||
{
|
||||
name: 'SVG',
|
||||
value: 'image/svg+xml',
|
||||
},
|
||||
],
|
||||
default: 'image/jpeg',
|
||||
description: 'Format used to export when downloading Google Drawings files',
|
||||
},
|
||||
{
|
||||
displayName: 'Google Slides',
|
||||
name: 'slidesToFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'MS PowerPoint',
|
||||
value:
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
},
|
||||
{
|
||||
name: 'OpenOffice Presentation',
|
||||
value: 'application/vnd.oasis.opendocument.presentation',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
value: 'application/pdf',
|
||||
},
|
||||
],
|
||||
default:
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
description: 'Format used to export when downloading Google Slides files',
|
||||
},
|
||||
{
|
||||
displayName: 'Google Sheets',
|
||||
name: 'sheetsToFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'CSV',
|
||||
value: 'text/csv',
|
||||
},
|
||||
{
|
||||
name: 'MS Excel',
|
||||
value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
},
|
||||
{
|
||||
name: 'Open Office Sheet',
|
||||
value: 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
value: 'application/pdf',
|
||||
},
|
||||
],
|
||||
default: 'text/csv',
|
||||
description: 'Format used to export when downloading Google Sheets files',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'File name. Ex: data.pdf.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['download'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
item: INodeExecutionData,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const downloadOptions = this.getNodeParameter('options', i);
|
||||
|
||||
const requestOptions = {
|
||||
useStream: true,
|
||||
returnFullResponse: true,
|
||||
encoding: 'arraybuffer',
|
||||
json: false,
|
||||
};
|
||||
|
||||
const file = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
{},
|
||||
{ fields: 'mimeType,name', supportsTeamDrives: true, supportsAllDrives: true },
|
||||
);
|
||||
let response;
|
||||
|
||||
if (file.mimeType?.includes('vnd.google-apps')) {
|
||||
const parameterKey = 'options.googleFileConversion.conversion';
|
||||
const type = file.mimeType.split('.')[2];
|
||||
let mime;
|
||||
if (type === 'document') {
|
||||
mime = this.getNodeParameter(
|
||||
`${parameterKey}.docsToFormat`,
|
||||
i,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
) as string;
|
||||
} else if (type === 'presentation') {
|
||||
mime = this.getNodeParameter(
|
||||
`${parameterKey}.slidesToFormat`,
|
||||
i,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
) as string;
|
||||
} else if (type === 'spreadsheet') {
|
||||
mime = this.getNodeParameter(
|
||||
`${parameterKey}.sheetsToFormat`,
|
||||
i,
|
||||
'application/x-vnd.oasis.opendocument.spreadsheet',
|
||||
) as string;
|
||||
} else {
|
||||
mime = this.getNodeParameter(`${parameterKey}.drawingsToFormat`, i, 'image/jpeg') as string;
|
||||
}
|
||||
response = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/drive/v3/files/${fileId}/export`,
|
||||
{},
|
||||
{ mimeType: mime, supportsAllDrives: true },
|
||||
undefined,
|
||||
requestOptions,
|
||||
);
|
||||
} else {
|
||||
response = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
{},
|
||||
{ alt: 'media', supportsAllDrives: true },
|
||||
undefined,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
|
||||
const mimeType =
|
||||
(response.headers as IDataObject)?.['content-type'] ?? file.mimeType ?? undefined;
|
||||
const fileName = downloadOptions.fileName ?? file.name ?? undefined;
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: item.json,
|
||||
binary: {},
|
||||
};
|
||||
|
||||
if (item.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 as IBinaryKeyData, item.binary);
|
||||
}
|
||||
|
||||
item = newItem;
|
||||
|
||||
const dataPropertyNameDownload = (downloadOptions.binaryPropertyName as string) || 'data';
|
||||
|
||||
item.binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
|
||||
response.body as Buffer,
|
||||
fileName as string,
|
||||
mimeType as string,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData([item], { itemData: { item: i } });
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { setParentFolder } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...fileRLC,
|
||||
description: 'The file to move',
|
||||
},
|
||||
{
|
||||
...driveRLC,
|
||||
displayName: 'Parent Drive',
|
||||
description: 'The drive where to move the file',
|
||||
},
|
||||
{
|
||||
...folderRLC,
|
||||
displayName: 'Parent Folder',
|
||||
description: 'The folder where to move the file',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['move'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, undefined, {
|
||||
extractValue: true,
|
||||
});
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const folderId = this.getNodeParameter('folderId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const qs = {
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
};
|
||||
|
||||
const { parents } = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
undefined,
|
||||
{
|
||||
...qs,
|
||||
fields: 'parents',
|
||||
},
|
||||
);
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
undefined,
|
||||
{
|
||||
...qs,
|
||||
addParents: setParentFolder(folderId, driveId),
|
||||
removeParents: ((parents as string[]) || []).join(','),
|
||||
},
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { fileRLC, permissionsOptions, shareOptions } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...fileRLC,
|
||||
description: 'The file to share',
|
||||
},
|
||||
permissionsOptions,
|
||||
shareOptions,
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['share'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const fileId = this.getNodeParameter('fileId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const permissions = this.getNodeParameter('permissionsUi', i) as IDataObject;
|
||||
|
||||
const shareOption = this.getNodeParameter('options', i);
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
const qs: IDataObject = {
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if (permissions.permissionsValues) {
|
||||
Object.assign(body, permissions.permissionsValues);
|
||||
}
|
||||
|
||||
Object.assign(qs, shareOption);
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/drive/v3/files/${fileId}/permissions`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import {
|
||||
getItemBinaryData,
|
||||
prepareQueryString,
|
||||
setFileProperties,
|
||||
setUpdateCommonParams,
|
||||
} from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { fileRLC, updateCommonOptions } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...fileRLC,
|
||||
displayName: 'File to Update',
|
||||
description: 'The file to update',
|
||||
},
|
||||
{
|
||||
displayName: 'Change File Content',
|
||||
name: 'changeFileContent',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to send a new binary data to update the file',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'inputDataFieldName',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. data',
|
||||
default: 'data',
|
||||
hint: 'The name of the input field containing the binary file data to update the file',
|
||||
description:
|
||||
'Find the name of input field containing the binary data to update the file in the Input panel on the left, in the Binary tab',
|
||||
displayOptions: {
|
||||
show: {
|
||||
changeFileContent: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'New Updated File Name',
|
||||
name: 'newUpdatedFileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. My New File',
|
||||
description: 'If not specified, the file name will not be changed',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
...updateCommonOptions,
|
||||
{
|
||||
displayName: 'Move to Trash',
|
||||
name: 'trashed',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to move a file to the trash. Only the owner may trash a file.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Fields',
|
||||
name: 'fields',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: '[All]',
|
||||
value: '*',
|
||||
description: 'All fields',
|
||||
},
|
||||
{
|
||||
name: 'explicitlyTrashed',
|
||||
value: 'explicitlyTrashed',
|
||||
},
|
||||
{
|
||||
name: 'exportLinks',
|
||||
value: 'exportLinks',
|
||||
},
|
||||
{
|
||||
name: 'hasThumbnail',
|
||||
value: 'hasThumbnail',
|
||||
},
|
||||
{
|
||||
name: 'iconLink',
|
||||
value: 'iconLink',
|
||||
},
|
||||
{
|
||||
name: 'ID',
|
||||
value: 'id',
|
||||
},
|
||||
{
|
||||
name: 'Kind',
|
||||
value: 'kind',
|
||||
},
|
||||
{
|
||||
name: 'mimeType',
|
||||
value: 'mimeType',
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: 'name',
|
||||
},
|
||||
{
|
||||
name: 'Permissions',
|
||||
value: 'permissions',
|
||||
},
|
||||
{
|
||||
name: 'Shared',
|
||||
value: 'shared',
|
||||
},
|
||||
{
|
||||
name: 'Spaces',
|
||||
value: 'spaces',
|
||||
},
|
||||
{
|
||||
name: 'Starred',
|
||||
value: 'starred',
|
||||
},
|
||||
{
|
||||
name: 'thumbnailLink',
|
||||
value: 'thumbnailLink',
|
||||
},
|
||||
{
|
||||
name: 'Trashed',
|
||||
value: 'trashed',
|
||||
},
|
||||
{
|
||||
name: 'Version',
|
||||
value: 'version',
|
||||
},
|
||||
{
|
||||
name: 'webViewLink',
|
||||
value: 'webViewLink',
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
description: 'The fields to return',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['update'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const changeFileContent = this.getNodeParameter('changeFileContent', i, false) as boolean;
|
||||
|
||||
let mimeType;
|
||||
|
||||
// update file binary data
|
||||
if (changeFileContent) {
|
||||
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
|
||||
|
||||
const binaryData = await getItemBinaryData.call(this, inputDataFieldName, i);
|
||||
|
||||
const { contentLength, fileContent } = binaryData;
|
||||
mimeType = binaryData.mimeType;
|
||||
|
||||
if (Buffer.isBuffer(fileContent)) {
|
||||
await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/upload/drive/v3/files/${fileId}`,
|
||||
fileContent,
|
||||
{
|
||||
uploadType: 'media',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
'Content-Length': contentLength,
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const resumableUpload = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/upload/drive/v3/files/${fileId}`,
|
||||
undefined,
|
||||
{ uploadType: 'resumable', supportsAllDrives: true },
|
||||
undefined,
|
||||
{
|
||||
returnFullResponse: true,
|
||||
},
|
||||
);
|
||||
const uploadUrl = resumableUpload.headers.location;
|
||||
|
||||
let offset = 0;
|
||||
for await (const chunk of fileContent) {
|
||||
const nextOffset = offset + Number(chunk.length);
|
||||
try {
|
||||
await this.helpers.httpRequest({
|
||||
method: 'PUT',
|
||||
url: uploadUrl,
|
||||
headers: {
|
||||
'Content-Length': chunk.length,
|
||||
'Content-Range': `bytes ${offset}-${nextOffset - 1}/${contentLength}`,
|
||||
},
|
||||
body: chunk,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 308) {
|
||||
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
|
||||
}
|
||||
}
|
||||
offset = nextOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const qs: IDataObject = setUpdateCommonParams(
|
||||
{
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (options.fields) {
|
||||
const queryFields = prepareQueryString(options.fields as string[]);
|
||||
qs.fields = queryFields;
|
||||
}
|
||||
|
||||
if (options.trashed) {
|
||||
qs.trashed = options.trashed;
|
||||
}
|
||||
|
||||
const body: IDataObject = setFileProperties({}, options);
|
||||
|
||||
const newUpdatedFileName = this.getNodeParameter('newUpdatedFileName', i, '') as string;
|
||||
if (newUpdatedFileName) {
|
||||
body.name = newUpdatedFileName;
|
||||
}
|
||||
|
||||
if (mimeType) {
|
||||
body.mimeType = mimeType;
|
||||
}
|
||||
|
||||
// update file metadata
|
||||
const responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import FormData from 'form-data';
|
||||
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import {
|
||||
getItemBinaryData,
|
||||
setFileProperties,
|
||||
setUpdateCommonParams,
|
||||
setParentFolder,
|
||||
processInChunks,
|
||||
} from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'inputDataFieldName',
|
||||
type: 'string',
|
||||
placeholder: '“e.g. data',
|
||||
default: 'data',
|
||||
required: true,
|
||||
hint: 'The name of the input field containing the binary file data to update the file',
|
||||
description:
|
||||
'Find the name of input field containing the binary data to update the file in the Input panel on the left, in the Binary tab',
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. My New File',
|
||||
description: 'If not specified, the original file name will be used',
|
||||
},
|
||||
{
|
||||
...driveRLC,
|
||||
displayName: 'Parent Drive',
|
||||
description: 'The drive where to upload the file',
|
||||
},
|
||||
{
|
||||
...folderRLC,
|
||||
displayName: 'Parent Folder',
|
||||
description: 'The folder where to upload the file',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
...updateCommonOptions,
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplifyOutput',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of all fields',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
|
||||
|
||||
const { contentLength, fileContent, originalFilename, mimeType } = await getItemBinaryData.call(
|
||||
this,
|
||||
inputDataFieldName,
|
||||
i,
|
||||
);
|
||||
|
||||
const name = (this.getNodeParameter('name', i) as string) || originalFilename;
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const folderId = this.getNodeParameter('folderId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
let uploadId;
|
||||
const metadata = {
|
||||
name,
|
||||
parents: [setParentFolder(folderId, driveId)],
|
||||
};
|
||||
if (Buffer.isBuffer(fileContent)) {
|
||||
const multiPartBody = new FormData();
|
||||
multiPartBody.append('metadata', JSON.stringify(metadata), {
|
||||
contentType: 'application/json',
|
||||
});
|
||||
multiPartBody.append('data', fileContent, {
|
||||
contentType: mimeType,
|
||||
knownLength: contentLength,
|
||||
});
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
multiPartBody.getBuffer(),
|
||||
{
|
||||
uploadType: 'multipart',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': `multipart/related; boundary=${multiPartBody.getBoundary()}`,
|
||||
'Content-Length': multiPartBody.getLengthSync(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
uploadId = response.id;
|
||||
} else {
|
||||
const resumableUpload = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
metadata,
|
||||
{
|
||||
uploadType: 'resumable',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
returnFullResponse: true,
|
||||
headers: {
|
||||
'X-Upload-Content-Type': mimeType,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const uploadUrl = resumableUpload.headers.location;
|
||||
|
||||
// 2MB chunks, needs to be a multiple of 256kB for Google Drive API
|
||||
const chunkSizeBytes = 2048 * 1024;
|
||||
|
||||
await processInChunks(fileContent, chunkSizeBytes, async (chunk, offset) => {
|
||||
try {
|
||||
const response = await this.helpers.httpRequest({
|
||||
method: 'PUT',
|
||||
url: uploadUrl,
|
||||
headers: {
|
||||
'Content-Length': chunk.length,
|
||||
'Content-Range': `bytes ${offset}-${offset + chunk.byteLength - 1}/${contentLength}`,
|
||||
},
|
||||
body: chunk,
|
||||
});
|
||||
uploadId = response?.id;
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 308) throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const qs = setUpdateCommonParams(
|
||||
{
|
||||
addParents: setParentFolder(folderId, driveId),
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (!options.simplifyOutput) {
|
||||
qs.fields = '*';
|
||||
}
|
||||
|
||||
const body = setFileProperties(
|
||||
{
|
||||
mimeType,
|
||||
name,
|
||||
originalFilename,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/drive/v3/files/${uploadId}`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as search from './search.operation';
|
||||
|
||||
export { search };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['fileFolder'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Search or list files and folders',
|
||||
action: 'Search files and folders',
|
||||
},
|
||||
],
|
||||
default: 'search',
|
||||
},
|
||||
...search.description,
|
||||
];
|
||||
@@ -0,0 +1,364 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type { SearchFilter } from '../../helpers/interfaces';
|
||||
import { DRIVE, RLC_FOLDER_DEFAULT } from '../../helpers/interfaces';
|
||||
import { prepareQueryString, updateDriveScopes } from '../../helpers/utils';
|
||||
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
|
||||
import { driveRLC, fileTypesOptions, folderRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Search Method',
|
||||
name: 'searchMethod',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Search File/Folder Name',
|
||||
value: 'name',
|
||||
},
|
||||
{
|
||||
name: 'Advanced Search',
|
||||
value: 'query',
|
||||
},
|
||||
],
|
||||
default: 'name',
|
||||
description: 'Whether to search for the file/folder name or use a query string',
|
||||
},
|
||||
{
|
||||
displayName: 'Search Query',
|
||||
name: 'queryString',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
searchMethod: ['name'],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. My File / My Folder',
|
||||
description:
|
||||
'The name of the file or folder to search for. Returns also files and folders whose names partially match this search term.',
|
||||
},
|
||||
{
|
||||
displayName: 'Query String',
|
||||
name: 'queryString',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
searchMethod: ['query'],
|
||||
},
|
||||
},
|
||||
placeholder: "e.g. not name contains 'hello'",
|
||||
description:
|
||||
'Use the Google query strings syntax to search for a specific set of files or folders. <a href="https://developers.google.com/drive/api/v3/search-files" target="_blank">Learn more</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Filter',
|
||||
name: 'filter',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
...driveRLC,
|
||||
description:
|
||||
'The drive you want to search in. By default, the personal "My Drive" is used.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
...folderRLC,
|
||||
description:
|
||||
'The folder you want to search in. By default, the root folder of the drive is used. If you select a folder other than the root folder, only the direct children will be included.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
displayName: 'What to Search',
|
||||
name: 'whatToSearch',
|
||||
type: 'options',
|
||||
default: 'all',
|
||||
options: [
|
||||
{
|
||||
name: 'Files and Folders',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
name: 'Files',
|
||||
value: 'files',
|
||||
},
|
||||
{
|
||||
name: 'Folders',
|
||||
value: 'folders',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'File Types',
|
||||
name: 'fileTypes',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
description: 'Return only items corresponding to the selected MIME types',
|
||||
options: fileTypesOptions,
|
||||
displayOptions: {
|
||||
show: {
|
||||
whatToSearch: ['all'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'File Types',
|
||||
name: 'fileTypes',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
description: 'Return only items corresponding to the selected MIME types',
|
||||
options: fileTypesOptions.filter((option) => option.name !== 'Folder'),
|
||||
displayOptions: {
|
||||
show: {
|
||||
whatToSearch: ['files'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Include Trashed Items',
|
||||
name: 'includeTrashed',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: "Whether to return also items in the Drive's bin",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'fields',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: '*',
|
||||
value: '*',
|
||||
description: 'All fields',
|
||||
},
|
||||
{
|
||||
name: 'explicitlyTrashed',
|
||||
value: 'explicitlyTrashed',
|
||||
},
|
||||
{
|
||||
name: 'exportLinks',
|
||||
value: 'exportLinks',
|
||||
},
|
||||
{
|
||||
name: 'hasThumbnail',
|
||||
value: 'hasThumbnail',
|
||||
},
|
||||
{
|
||||
name: 'iconLink',
|
||||
value: 'iconLink',
|
||||
},
|
||||
{
|
||||
name: 'ID',
|
||||
value: 'id',
|
||||
},
|
||||
{
|
||||
name: 'Kind',
|
||||
value: 'kind',
|
||||
},
|
||||
{
|
||||
name: 'mimeType',
|
||||
value: 'mimeType',
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: 'name',
|
||||
},
|
||||
{
|
||||
name: 'Permissions',
|
||||
value: 'permissions',
|
||||
},
|
||||
{
|
||||
name: 'Shared',
|
||||
value: 'shared',
|
||||
},
|
||||
{
|
||||
name: 'Spaces',
|
||||
value: 'spaces',
|
||||
},
|
||||
{
|
||||
name: 'Starred',
|
||||
value: 'starred',
|
||||
},
|
||||
{
|
||||
name: 'thumbnailLink',
|
||||
value: 'thumbnailLink',
|
||||
},
|
||||
{
|
||||
name: 'Trashed',
|
||||
value: 'trashed',
|
||||
},
|
||||
{
|
||||
name: 'Version',
|
||||
value: 'version',
|
||||
},
|
||||
{
|
||||
name: 'webViewLink',
|
||||
value: 'webViewLink',
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
description: 'The fields to return',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['fileFolder'],
|
||||
operation: ['search'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const searchMethod = this.getNodeParameter('searchMethod', i) as string;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const query = [];
|
||||
|
||||
const queryString = this.getNodeParameter('queryString', i) as string;
|
||||
|
||||
if (searchMethod === 'name') {
|
||||
query.push(`name contains '${queryString}'`);
|
||||
} else {
|
||||
query.push(queryString);
|
||||
}
|
||||
|
||||
const filter = this.getNodeParameter('filter', i, {}) as SearchFilter;
|
||||
|
||||
let driveId = '';
|
||||
let folderId = '';
|
||||
const returnedTypes: string[] = [];
|
||||
|
||||
if (Object.keys(filter)?.length) {
|
||||
if (filter.folderId) {
|
||||
if (filter.folderId.mode === 'url') {
|
||||
folderId = this.getNodeParameter('filter.folderId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
} else {
|
||||
folderId = filter.folderId.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (folderId && folderId !== RLC_FOLDER_DEFAULT) {
|
||||
query.push(`'${folderId}' in parents`);
|
||||
}
|
||||
|
||||
if (filter.driveId) {
|
||||
let value;
|
||||
if (filter.driveId.mode === 'url') {
|
||||
value = this.getNodeParameter('filter.driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
} else {
|
||||
value = filter.driveId.value;
|
||||
}
|
||||
driveId = value;
|
||||
}
|
||||
|
||||
const whatToSearch = filter.whatToSearch || 'all';
|
||||
if (whatToSearch === 'folders') {
|
||||
query.push(`mimeType = '${DRIVE.FOLDER}'`);
|
||||
} else {
|
||||
if (whatToSearch === 'files') {
|
||||
query.push(`mimeType != '${DRIVE.FOLDER}'`);
|
||||
}
|
||||
|
||||
if (filter?.fileTypes?.length && !filter.fileTypes.includes('*')) {
|
||||
filter.fileTypes.forEach((fileType: string) => {
|
||||
returnedTypes.push(`mimeType = '${fileType}'`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!filter.includeTrashed) {
|
||||
query.push('trashed = false');
|
||||
}
|
||||
}
|
||||
|
||||
if (returnedTypes.length) {
|
||||
query.push(`(${returnedTypes.join(' or ')})`);
|
||||
}
|
||||
|
||||
const queryFields = prepareQueryString(options.fields as string[]);
|
||||
|
||||
const qs: IDataObject = {
|
||||
fields: `nextPageToken, files(${queryFields})`,
|
||||
q: query.filter((q) => q).join(' and '),
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
};
|
||||
|
||||
updateDriveScopes(qs, driveId);
|
||||
|
||||
if (!driveId && folderId === RLC_FOLDER_DEFAULT) {
|
||||
qs.corpora = 'user';
|
||||
qs.spaces = 'drive';
|
||||
qs.includeItemsFromAllDrives = false;
|
||||
qs.supportsAllDrives = false;
|
||||
}
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i, false);
|
||||
|
||||
let response;
|
||||
if (returnAll) {
|
||||
response = await googleApiRequestAllItems.call(this, 'GET', 'files', '/drive/v3/files', {}, qs);
|
||||
} else {
|
||||
qs.pageSize = this.getNodeParameter('limit', i);
|
||||
response = await googleApiRequest.call(this, 'GET', '/drive/v3/files', undefined, qs);
|
||||
response = response.files;
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as deleteFolder from './deleteFolder.operation';
|
||||
import * as share from './share.operation';
|
||||
|
||||
export { create, deleteFolder, share };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['folder'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a folder',
|
||||
action: 'Create folder',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'deleteFolder',
|
||||
description: 'Permanently delete a folder',
|
||||
action: 'Delete folder',
|
||||
},
|
||||
{
|
||||
name: 'Share',
|
||||
value: 'share',
|
||||
description: 'Add sharing permissions to a folder',
|
||||
action: 'Share folder',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
...create.description,
|
||||
...deleteFolder.description,
|
||||
...share.description,
|
||||
];
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { DRIVE } from '../../helpers/interfaces';
|
||||
import { setParentFolder } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, folderRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Folder Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. New Folder',
|
||||
description: "The name of the new folder. If not set, 'Untitled' will be used.",
|
||||
},
|
||||
{
|
||||
...driveRLC,
|
||||
displayName: 'Parent Drive',
|
||||
description: 'The drive where to create the new folder',
|
||||
},
|
||||
{
|
||||
...folderRLC,
|
||||
displayName: 'Parent Folder',
|
||||
description: 'The parent folder where to create the new folder',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplifyOutput',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of all fields',
|
||||
},
|
||||
{
|
||||
displayName: 'Folder Color',
|
||||
name: 'folderColorRgb',
|
||||
type: 'color',
|
||||
default: '',
|
||||
description:
|
||||
'The color of the folder as an RGB hex string. If an unsupported color is specified, the closest color in the palette will be used instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['folder'],
|
||||
operation: ['create'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const name = (this.getNodeParameter('name', i) as string) || 'Untitled';
|
||||
|
||||
const driveId = this.getNodeParameter('driveId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const folderId = this.getNodeParameter('folderId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const body: IDataObject = {
|
||||
name,
|
||||
mimeType: DRIVE.FOLDER,
|
||||
parents: [setParentFolder(folderId, driveId)],
|
||||
};
|
||||
|
||||
const folderColorRgb =
|
||||
(this.getNodeParameter('options.folderColorRgb', i, '') as string) || undefined;
|
||||
if (folderColorRgb) {
|
||||
body.folderColorRgb = folderColorRgb;
|
||||
}
|
||||
|
||||
const simplifyOutput = this.getNodeParameter('options.simplifyOutput', i, true) as boolean;
|
||||
let fields;
|
||||
if (!simplifyOutput) {
|
||||
fields = '*';
|
||||
}
|
||||
|
||||
const qs = {
|
||||
fields,
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
};
|
||||
|
||||
const response = await googleApiRequest.call(this, 'POST', '/drive/v3/files', body, qs);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { folderNoRootRLC } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...folderNoRootRLC,
|
||||
description: 'The folder to delete',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Delete Permanently',
|
||||
name: 'deletePermanently',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to delete the folder immediately. If false, the folder will be moved to the trash.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['folder'],
|
||||
operation: ['deleteFolder'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const folderId = this.getNodeParameter('folderNoRootId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const deletePermanently = this.getNodeParameter('options.deletePermanently', i, false) as boolean;
|
||||
|
||||
const qs = {
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if (deletePermanently) {
|
||||
await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${folderId}`, undefined, qs);
|
||||
} else {
|
||||
await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/drive/v3/files/${folderId}`,
|
||||
{ trashed: true },
|
||||
qs,
|
||||
);
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({
|
||||
fileId: folderId,
|
||||
success: true,
|
||||
}),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { folderNoRootRLC, permissionsOptions, shareOptions } from '../common.descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...folderNoRootRLC,
|
||||
description: 'The folder to share',
|
||||
},
|
||||
permissionsOptions,
|
||||
shareOptions,
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['folder'],
|
||||
operation: ['share'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const folderId = this.getNodeParameter('folderNoRootId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const permissions = this.getNodeParameter('permissionsUi', i) as IDataObject;
|
||||
|
||||
const shareOption = this.getNodeParameter('options', i);
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
const qs: IDataObject = {
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if (permissions.permissionsValues) {
|
||||
Object.assign(body, permissions.permissionsValues);
|
||||
}
|
||||
|
||||
Object.assign(qs, shareOption);
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/drive/v3/files/${folderId}/permissions`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AllEntities } from 'n8n-workflow';
|
||||
|
||||
type NodeMap = {
|
||||
drive: 'create' | 'deleteDrive' | 'get' | 'list' | 'update';
|
||||
file:
|
||||
| 'copy'
|
||||
| 'createFromText'
|
||||
| 'download'
|
||||
| 'deleteFile'
|
||||
| 'move'
|
||||
| 'share'
|
||||
| 'upload'
|
||||
| 'update';
|
||||
folder: 'create' | 'deleteFolder' | 'share';
|
||||
fileFolder: 'search';
|
||||
};
|
||||
|
||||
export type GoogleDriveType = AllEntities<NodeMap>;
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import * as drive from './drive/Drive.resource';
|
||||
import * as file from './file/File.resource';
|
||||
import * as fileFolder from './fileFolder/FileFolder.resource';
|
||||
import * as folder from './folder/Folder.resource';
|
||||
import type { GoogleDriveType } from './node.type';
|
||||
|
||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const resource = this.getNodeParameter<GoogleDriveType>('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
const googleDrive = {
|
||||
resource,
|
||||
operation,
|
||||
} as GoogleDriveType;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
switch (googleDrive.resource) {
|
||||
case 'drive':
|
||||
returnData.push(...(await drive[googleDrive.operation].execute.call(this, i)));
|
||||
break;
|
||||
case 'file':
|
||||
returnData.push(...(await file[googleDrive.operation].execute.call(this, i, items[i])));
|
||||
break;
|
||||
case 'fileFolder':
|
||||
returnData.push(...(await fileFolder[googleDrive.operation].execute.call(this, i)));
|
||||
break;
|
||||
case 'folder':
|
||||
returnData.push(...(await folder[googleDrive.operation].execute.call(this, i)));
|
||||
break;
|
||||
default:
|
||||
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
if (resource === 'file' && operation === 'download') {
|
||||
items[i].json = { error: error.message };
|
||||
} else {
|
||||
returnData.push({ json: { error: error.message } });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as drive from './drive/Drive.resource';
|
||||
import * as file from './file/File.resource';
|
||||
import * as fileFolder from './fileFolder/FileFolder.resource';
|
||||
import * as folder from './folder/Folder.resource';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Google Drive',
|
||||
name: 'googleDrive',
|
||||
icon: 'file:googleDrive.svg',
|
||||
group: ['input'],
|
||||
version: 3,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Access data on Google Drive',
|
||||
defaults: {
|
||||
name: 'Google Drive',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
usableAsTool: true,
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['serviceAccount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'googleDriveOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'OAuth2 (recommended)',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
{
|
||||
name: 'Service Account',
|
||||
value: 'serviceAccount',
|
||||
},
|
||||
],
|
||||
default: 'oAuth2',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'File',
|
||||
value: 'file',
|
||||
},
|
||||
{
|
||||
name: 'File/Folder',
|
||||
value: 'fileFolder',
|
||||
},
|
||||
{
|
||||
name: 'Folder',
|
||||
value: 'folder',
|
||||
},
|
||||
{
|
||||
name: 'Shared Drive',
|
||||
value: 'drive',
|
||||
},
|
||||
],
|
||||
default: 'file',
|
||||
},
|
||||
...drive.description,
|
||||
...file.description,
|
||||
...fileFolder.description,
|
||||
...folder.description,
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export const UPLOAD_CHUNK_SIZE = 256 * 1024;
|
||||
|
||||
export type SearchFilter = {
|
||||
driveId?: {
|
||||
value: string;
|
||||
mode: string;
|
||||
};
|
||||
folderId?: {
|
||||
value: string;
|
||||
mode: string;
|
||||
};
|
||||
whatToSearch?: 'all' | 'files' | 'folders';
|
||||
fileTypes?: string[];
|
||||
includeTrashed?: boolean;
|
||||
};
|
||||
|
||||
export const RLC_DRIVE_DEFAULT = 'My Drive';
|
||||
export const RLC_FOLDER_DEFAULT = 'root';
|
||||
|
||||
export const DRIVE = {
|
||||
FOLDER: 'application/vnd.google-apps.folder',
|
||||
AUDIO: 'application/vnd.google-apps.audio',
|
||||
DOCUMENT: 'application/vnd.google-apps.document',
|
||||
SDK: 'application/vnd.google-apps.drive-sdk',
|
||||
DRAWING: 'application/vnd.google-apps.drawing',
|
||||
FILE: 'application/vnd.google-apps.file',
|
||||
FORM: 'application/vnd.google-apps.form',
|
||||
FUSIONTABLE: 'application/vnd.google-apps.fusiontable',
|
||||
MAP: 'application/vnd.google-apps.map',
|
||||
PHOTO: 'application/vnd.google-apps.photo',
|
||||
PRESENTATION: 'application/vnd.google-apps.presentation',
|
||||
APP_SCRIPTS: 'application/vnd.google-apps.script',
|
||||
SITES: 'application/vnd.google-apps.sites',
|
||||
SPREADSHEET: 'application/vnd.google-apps.spreadsheet',
|
||||
UNKNOWN: 'application/vnd.google-apps.unknown',
|
||||
VIDEO: 'application/vnd.google-apps.video',
|
||||
} as const;
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
||||
import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
import { RLC_DRIVE_DEFAULT, RLC_FOLDER_DEFAULT, UPLOAD_CHUNK_SIZE } from './interfaces';
|
||||
|
||||
export function prepareQueryString(fields: string[] | undefined) {
|
||||
let queryFields = 'id, name';
|
||||
if (fields) {
|
||||
if (fields.includes('*')) {
|
||||
queryFields = '*';
|
||||
} else {
|
||||
queryFields = fields.join(', ');
|
||||
}
|
||||
}
|
||||
return queryFields;
|
||||
}
|
||||
|
||||
export async function getItemBinaryData(
|
||||
this: IExecuteFunctions,
|
||||
inputDataFieldName: string,
|
||||
i: number,
|
||||
chunkSize = UPLOAD_CHUNK_SIZE,
|
||||
) {
|
||||
let contentLength: number;
|
||||
let fileContent: Buffer | Readable;
|
||||
let originalFilename: string | undefined;
|
||||
let mimeType;
|
||||
|
||||
if (!inputDataFieldName) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'The name of the input field containing the binary file data must be set',
|
||||
{
|
||||
itemIndex: i,
|
||||
},
|
||||
);
|
||||
}
|
||||
const binaryData = this.helpers.assertBinaryData(i, inputDataFieldName);
|
||||
|
||||
if (binaryData.id) {
|
||||
// Stream data in 256KB chunks, and upload the via the resumable upload api
|
||||
fileContent = await this.helpers.getBinaryStream(binaryData.id, chunkSize);
|
||||
const metadata = await this.helpers.getBinaryMetadata(binaryData.id);
|
||||
contentLength = metadata.fileSize;
|
||||
originalFilename = metadata.fileName;
|
||||
if (metadata.mimeType) mimeType = binaryData.mimeType;
|
||||
} else {
|
||||
fileContent = Buffer.from(binaryData.data, BINARY_ENCODING);
|
||||
contentLength = fileContent.length;
|
||||
originalFilename = binaryData.fileName;
|
||||
mimeType = binaryData.mimeType;
|
||||
}
|
||||
|
||||
return {
|
||||
contentLength,
|
||||
fileContent,
|
||||
originalFilename,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
export function setFileProperties(body: IDataObject, options: IDataObject) {
|
||||
if (options.propertiesUi) {
|
||||
const values = ((options.propertiesUi as IDataObject).propertyValues as IDataObject[]) || [];
|
||||
|
||||
body.properties = values.reduce(
|
||||
(acc, value) => Object.assign(acc, { [`${value.key}`]: value.value }),
|
||||
{} as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.appPropertiesUi) {
|
||||
const values =
|
||||
((options.appPropertiesUi as IDataObject).appPropertyValues as IDataObject[]) || [];
|
||||
|
||||
body.appProperties = values.reduce(
|
||||
(acc, value) => Object.assign(acc, { [`${value.key}`]: value.value }),
|
||||
{} as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export function setUpdateCommonParams(qs: IDataObject, options: IDataObject) {
|
||||
if (options.keepRevisionForever) {
|
||||
qs.keepRevisionForever = options.keepRevisionForever;
|
||||
}
|
||||
|
||||
if (options.ocrLanguage) {
|
||||
qs.ocrLanguage = options.ocrLanguage;
|
||||
}
|
||||
|
||||
if (options.useContentAsIndexableText) {
|
||||
qs.useContentAsIndexableText = options.useContentAsIndexableText;
|
||||
}
|
||||
|
||||
return qs;
|
||||
}
|
||||
|
||||
export function updateDriveScopes(
|
||||
qs: IDataObject,
|
||||
driveId: string,
|
||||
defaultDrive = RLC_DRIVE_DEFAULT,
|
||||
) {
|
||||
if (driveId) {
|
||||
if (driveId === defaultDrive) {
|
||||
qs.includeItemsFromAllDrives = false;
|
||||
qs.supportsAllDrives = false;
|
||||
qs.spaces = 'appDataFolder, drive';
|
||||
qs.corpora = 'user';
|
||||
} else {
|
||||
qs.driveId = driveId;
|
||||
qs.corpora = 'drive';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setParentFolder(
|
||||
folderId: string,
|
||||
driveId: string,
|
||||
folderIdDefault = RLC_FOLDER_DEFAULT,
|
||||
driveIdDefault = RLC_DRIVE_DEFAULT,
|
||||
) {
|
||||
if (folderId !== folderIdDefault) {
|
||||
return folderId;
|
||||
} else if (driveId && driveId !== driveIdDefault) {
|
||||
return driveId;
|
||||
} else {
|
||||
return 'root';
|
||||
}
|
||||
}
|
||||
|
||||
export async function processInChunks(
|
||||
stream: Readable,
|
||||
chunkSize: number,
|
||||
process: (chunk: Buffer, offset: number) => void | Promise<void>,
|
||||
) {
|
||||
let buffer = Buffer.alloc(0);
|
||||
let offset = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
|
||||
while (buffer.length >= chunkSize) {
|
||||
const chunkToProcess = buffer.subarray(0, chunkSize);
|
||||
await process(chunkToProcess, offset);
|
||||
|
||||
buffer = buffer.subarray(chunkSize);
|
||||
offset += chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Process last chunk, could be smaller than chunkSize
|
||||
if (buffer.length > 0) {
|
||||
await process(buffer, offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as listSearch from './listSearch';
|
||||
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { SearchFilter } from '../helpers/interfaces';
|
||||
import { DRIVE, RLC_DRIVE_DEFAULT, RLC_FOLDER_DEFAULT } from '../helpers/interfaces';
|
||||
import { updateDriveScopes } from '../helpers/utils';
|
||||
import { googleApiRequest } from '../transport';
|
||||
|
||||
interface FilesItem {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
webViewLink: string;
|
||||
}
|
||||
|
||||
interface DriveItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function fileSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const query: string[] = ['trashed = false'];
|
||||
if (filter) {
|
||||
query.push(`name contains '${filter.replace("'", "\\'")}'`);
|
||||
}
|
||||
query.push(`mimeType != '${DRIVE.FOLDER}'`);
|
||||
const res = await googleApiRequest.call(this, 'GET', '/drive/v3/files', undefined, {
|
||||
q: query.join(' and '),
|
||||
pageToken: paginationToken,
|
||||
fields: 'nextPageToken,files(id,name,mimeType,webViewLink)',
|
||||
orderBy: 'name_natural',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
});
|
||||
return {
|
||||
results: res.files.map((file: FilesItem) => ({
|
||||
name: file.name,
|
||||
value: file.id,
|
||||
url: file.webViewLink,
|
||||
})),
|
||||
paginationToken: res.nextPageToken,
|
||||
};
|
||||
}
|
||||
|
||||
export async function driveSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
let res = { drives: [], nextPageToken: undefined };
|
||||
|
||||
res = await googleApiRequest.call(this, 'GET', '/drive/v3/drives', undefined, {
|
||||
q: filter ? `name contains '${filter.replace("'", "\\'")}'` : undefined,
|
||||
pageToken: paginationToken,
|
||||
});
|
||||
|
||||
const results: INodeListSearchItems[] = [];
|
||||
|
||||
res.drives.forEach((drive: DriveItem) => {
|
||||
results.push({
|
||||
name: drive.name,
|
||||
value: drive.id,
|
||||
url: `https://drive.google.com/drive/folders/${drive.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
paginationToken: res.nextPageToken,
|
||||
};
|
||||
}
|
||||
|
||||
export async function driveSearchWithDefault(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const drives = await driveSearch.call(this, filter, paginationToken);
|
||||
|
||||
let results: INodeListSearchItems[] = [];
|
||||
|
||||
if (filter && !RLC_DRIVE_DEFAULT.toLowerCase().includes(filter.toLowerCase())) {
|
||||
results = drives.results;
|
||||
} else {
|
||||
results = [
|
||||
{
|
||||
name: RLC_DRIVE_DEFAULT,
|
||||
value: RLC_DRIVE_DEFAULT,
|
||||
url: 'https://drive.google.com/drive/my-drive',
|
||||
},
|
||||
...drives.results,
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
paginationToken: drives.paginationToken,
|
||||
};
|
||||
}
|
||||
|
||||
export async function folderSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const query: string[] = [];
|
||||
if (filter) {
|
||||
query.push(`name contains '${filter.replace("'", "\\'")}'`);
|
||||
}
|
||||
query.push(`mimeType = '${DRIVE.FOLDER}'`);
|
||||
|
||||
const qs: IDataObject = {
|
||||
q: query.join(' and '),
|
||||
pageToken: paginationToken,
|
||||
fields: 'nextPageToken,files(id,name,mimeType,webViewLink,parents,driveId)',
|
||||
orderBy: 'name_natural',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
};
|
||||
|
||||
let driveId;
|
||||
|
||||
driveId = this.getNodeParameter('driveId', '') as IDataObject;
|
||||
|
||||
if (!driveId) {
|
||||
const searchFilter = this.getNodeParameter('filter', {}) as SearchFilter;
|
||||
if (searchFilter?.driveId?.mode === 'url') {
|
||||
searchFilter.driveId.value = this.getNodeParameter('filter.folderId', undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
}
|
||||
driveId = searchFilter.driveId;
|
||||
}
|
||||
updateDriveScopes(qs, driveId?.value as string);
|
||||
|
||||
const res = await googleApiRequest.call(this, 'GET', '/drive/v3/files', undefined, qs);
|
||||
|
||||
const results: INodeListSearchItems[] = [];
|
||||
|
||||
res.files.forEach((i: FilesItem) => {
|
||||
results.push({
|
||||
name: i.name,
|
||||
value: i.id,
|
||||
url: i.webViewLink,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
paginationToken: res.nextPageToken,
|
||||
};
|
||||
}
|
||||
|
||||
export async function folderSearchWithDefault(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const folders = await folderSearch.call(this, filter, paginationToken);
|
||||
|
||||
let results: INodeListSearchItems[] = [];
|
||||
const rootDefaultDisplayName = '/ (Root folder)';
|
||||
|
||||
if (
|
||||
filter &&
|
||||
!(
|
||||
RLC_FOLDER_DEFAULT.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
rootDefaultDisplayName.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
) {
|
||||
results = folders.results;
|
||||
} else {
|
||||
results = [
|
||||
{
|
||||
name: rootDefaultDisplayName,
|
||||
value: RLC_FOLDER_DEFAULT,
|
||||
url: 'https://drive.google.com/drive',
|
||||
},
|
||||
...folders.results,
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
paginationToken: folders.paginationToken,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Google Drive V2', () => {
|
||||
const credentials = {
|
||||
googleDriveOAuth2Api: {
|
||||
scope: 'https://www.googleapis.com/auth/drive',
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Folder Create Operation', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock folder creation
|
||||
mock
|
||||
.post('/drive/v3/files')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
name: 'Test Folder',
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
createdTime: '2024-01-01T00:00:00.000Z',
|
||||
modifiedTime: '2024-01-01T00:00:00.000Z',
|
||||
webViewLink:
|
||||
'https://drive.google.com/drive/folders/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['folder-create-basic.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
// Note: File Upload operations are skipped from workflow tests due to binary data complexity
|
||||
// The upload functionality is thoroughly tested in the dedicated unit tests
|
||||
|
||||
describe('File Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock file copy - correct endpoint
|
||||
mock
|
||||
.post('/drive/v3/files/123/copy')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'copy123',
|
||||
name: 'Copy of Test File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock file createFromText - multipart upload
|
||||
mock
|
||||
.post('/upload/drive/v3/files')
|
||||
.query({ uploadType: 'multipart', supportsAllDrives: true })
|
||||
.reply(200, {
|
||||
id: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
name: 'Test Text File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock metadata update after createFromText
|
||||
mock
|
||||
.patch('/drive/v3/files/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
name: 'Test Text File',
|
||||
mimeType: 'text/plain',
|
||||
size: '48',
|
||||
createdTime: '2024-01-01T00:00:00.000Z',
|
||||
modifiedTime: '2024-01-01T00:00:00.000Z',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock file download - metadata fetch first
|
||||
mock
|
||||
.get('/drive/v3/files/123')
|
||||
.query({ fields: 'mimeType,name', supportsTeamDrives: true, supportsAllDrives: true })
|
||||
.reply(200, {
|
||||
id: '123',
|
||||
name: 'Test File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock file download - content download
|
||||
mock
|
||||
.get('/drive/v3/files/123')
|
||||
.query({ alt: 'media', supportsAllDrives: true })
|
||||
.reply(200, 'Hello World', {
|
||||
'Content-Type': 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock.delete('/drive/v3/files/123').query(true).reply(200).persist();
|
||||
|
||||
mock
|
||||
.get('/drive/v3/files/123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '123',
|
||||
name: 'Test File',
|
||||
mimeType: 'text/plain',
|
||||
size: '1024',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.patch('/drive/v3/files/123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '123',
|
||||
name: 'Updated Test File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.post('/drive/v3/files/123/permissions')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'permission123',
|
||||
role: 'reader',
|
||||
type: 'user',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: [
|
||||
'file-copy.workflow.json',
|
||||
'file-createFromText.workflow.json',
|
||||
'file-delete.workflow.json',
|
||||
'file-download.workflow.json',
|
||||
'file-move.workflow.json',
|
||||
'file-share.workflow.json',
|
||||
'file-update.workflow.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Folder Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock folder delete
|
||||
mock.delete('/drive/v3/files/folder123').query(true).reply(200).persist();
|
||||
|
||||
// Mock folder share
|
||||
mock
|
||||
.post('/drive/v3/files/folder123/permissions')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'permission123',
|
||||
role: 'reader',
|
||||
type: 'user',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['folder-delete.workflow.json', 'folder-share.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('File/Folder Search Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock search
|
||||
mock
|
||||
.get('/drive/v3/files')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
files: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Test File',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
{
|
||||
id: 'folder123',
|
||||
name: 'Test Folder',
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
},
|
||||
],
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['filefolder-search.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shared Drive Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock drive operations
|
||||
mock
|
||||
.post('/drive/v3/drives')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'drive123',
|
||||
name: 'Test Drive',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock.delete('/drive/v3/drives/drive123').query(true).reply(200).persist();
|
||||
|
||||
mock
|
||||
.get('/drive/v3/drives/drive123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'drive123',
|
||||
name: 'Test Drive',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.get('/drive/v3/drives')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
drives: [
|
||||
{
|
||||
id: 'drive123',
|
||||
name: 'Test Drive',
|
||||
},
|
||||
],
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.patch('/drive/v3/drives/drive123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'drive123',
|
||||
name: 'Updated Test Drive',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: [
|
||||
'drive-create.workflow.json',
|
||||
'drive-delete.workflow.json',
|
||||
'drive-get.workflow.json',
|
||||
'drive-list.workflow.json',
|
||||
'drive-update.workflow.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Create Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "create",
|
||||
"name": "Test Drive"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-create",
|
||||
"name": "Create Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create Drive": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "deleteDrive",
|
||||
"driveId": {
|
||||
"__rl": true,
|
||||
"value": "drive123",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-delete",
|
||||
"name": "Delete Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Drive": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Get Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "get",
|
||||
"driveId": {
|
||||
"__rl": true,
|
||||
"value": "drive123",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-get",
|
||||
"name": "Get Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Drive": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive List Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "list"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-list",
|
||||
"name": "List Drives",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"List Drives": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "List Drives",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Update Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "update",
|
||||
"driveId": {
|
||||
"__rl": true,
|
||||
"value": "drive123",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {
|
||||
"name": "Updated Test Drive"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-update",
|
||||
"name": "Update Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Update Drive": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Updated Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Copy Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "copy",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"name": "Copy of Test File"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-copy",
|
||||
"name": "Copy File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Copy File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "copy123",
|
||||
"name": "Copy of Test File",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Copy File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Create From Text Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "createFromText",
|
||||
"name": "Test Text File",
|
||||
"content": "Hello World! This is a test file created from text."
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-createFromText",
|
||||
"name": "Create File From Text",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create File From Text": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create File From Text",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "deleteFile",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {
|
||||
"deletePermanently": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-delete",
|
||||
"name": "Delete File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Download Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "download",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-download",
|
||||
"name": "Download File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Download File": [
|
||||
{
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Download File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Move Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "move",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"folderId": {
|
||||
"__rl": true,
|
||||
"value": "folder456",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-move",
|
||||
"name": "Move File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Move File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"mimeType": "text/plain",
|
||||
"name": "Updated Test File"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Move File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Share Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "share",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"permissionsUi": {
|
||||
"permissionsValues": {
|
||||
"role": "reader",
|
||||
"type": "user",
|
||||
"emailAddress": "test@example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-share",
|
||||
"name": "Share File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Share File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "permission123",
|
||||
"role": "reader",
|
||||
"type": "user"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Share File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Update Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "update",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"newUpdatedFileName": "Updated Test File"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-update",
|
||||
"name": "Update File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Update File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"name": "Updated Test File",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"_comment": "This workflow tests basic file upload functionality with small embedded binary data. It uses the regular upload path (not resumable), as resumable uploads require binary data with an 'id' field which is not easily testable in workflow tests. Large file upload functionality is covered by dedicated unit tests.",
|
||||
"name": "Google Drive V2 File Upload Basic Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "upload",
|
||||
"inputDataFieldName": "data",
|
||||
"name": "small-file.pdf"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-upload-basic",
|
||||
"name": "Upload Small File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"When clicking 'Execute Workflow'": [
|
||||
{
|
||||
"json": {},
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PAovTGVuZ3RoIDYgMCBSCi9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nEWQwQrCMBBE",
|
||||
"mimeType": "application/pdf",
|
||||
"fileName": "small-file.pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Upload Small File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "small-file.pdf",
|
||||
"mimeType": "application/pdf",
|
||||
"size": "81",
|
||||
"createdTime": "2024-01-01T00:00:00.000Z",
|
||||
"modifiedTime": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Upload Small File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "Google Drive V2 File/Folder Search Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "fileFolder",
|
||||
"operation": "search",
|
||||
"searchMethod": "query",
|
||||
"queryString": "name contains 'test'"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "filefolder-search",
|
||||
"name": "Search Files and Folders",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Search Files and Folders": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"name": "Test File",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "folder123",
|
||||
"name": "Test Folder",
|
||||
"mimeType": "application/vnd.google-apps.folder"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search Files and Folders",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "Google Drive V2 Folder Create Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "folder",
|
||||
"operation": "create",
|
||||
"name": "Test Folder"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "folder-create-basic",
|
||||
"name": "Create Folder",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create Folder": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Folder",
|
||||
"mimeType": "application/vnd.google-apps.folder",
|
||||
"createdTime": "2024-01-01T00:00:00.000Z",
|
||||
"modifiedTime": "2024-01-01T00:00:00.000Z",
|
||||
"webViewLink": "https://drive.google.com/drive/folders/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Folder",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Google Drive V2 Folder Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "folder",
|
||||
"operation": "deleteFolder",
|
||||
"folderNoRootId": {
|
||||
"__rl": true,
|
||||
"value": "folder123",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {
|
||||
"deletePermanently": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "folder-delete",
|
||||
"name": "Delete Folder",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Folder": [
|
||||
{
|
||||
"json": {
|
||||
"fileId": "folder123",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Folder",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "Google Drive V2 Folder Share Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "folder",
|
||||
"operation": "share",
|
||||
"folderNoRootId": {
|
||||
"__rl": true,
|
||||
"value": "folder123",
|
||||
"mode": "id"
|
||||
},
|
||||
"permissionsUi": {
|
||||
"permissionsValues": {
|
||||
"role": "reader",
|
||||
"type": "user",
|
||||
"emailAddress": "test@example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "folder-share",
|
||||
"name": "Share Folder",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Share Folder": [
|
||||
{
|
||||
"json": {
|
||||
"id": "permission123",
|
||||
"role": "reader",
|
||||
"type": "user"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Share Folder",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IDataObject,
|
||||
IPollFunctions,
|
||||
JsonObject,
|
||||
IHttpRequestOptions,
|
||||
IHttpRequestMethods,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getGoogleAccessToken } from '../../../GenericFunctions';
|
||||
|
||||
export async function googleApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: IDataObject | string | Buffer = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
option: IDataObject = {},
|
||||
) {
|
||||
const authenticationMethod = this.getNodeParameter(
|
||||
'authentication',
|
||||
0,
|
||||
'serviceAccount',
|
||||
) as string;
|
||||
|
||||
let options: IHttpRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
url: uri || `https://www.googleapis.com${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
options = Object.assign({}, options, option);
|
||||
try {
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
if (authenticationMethod === 'serviceAccount') {
|
||||
const credentials = await this.getCredentials('googleApi');
|
||||
|
||||
const { access_token } = await getGoogleAccessToken.call(this, credentials, 'drive');
|
||||
|
||||
options.headers!.Authorization = `Bearer ${access_token}`;
|
||||
return await this.helpers.httpRequest(options);
|
||||
} else {
|
||||
return await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
'googleDriveOAuth2Api',
|
||||
options,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
|
||||
error.statusCode = '401';
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function googleApiRequestAllItems(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
propertyName: string,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
query: IDataObject = {},
|
||||
) {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
query.maxResults = query.maxResults || 100;
|
||||
query.pageSize = query.pageSize || 100;
|
||||
|
||||
do {
|
||||
responseData = await googleApiRequest.call(this, method, endpoint, body, query);
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
|
||||
if (responseData.nextPageToken) {
|
||||
query.pageToken = responseData.nextPageToken as string;
|
||||
}
|
||||
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
|
||||
|
||||
return returnData;
|
||||
}
|
||||
Reference in New Issue
Block a user