feat(Google Drive Node): Overhaul (#5941)

This commit is contained in:
Michael Kret 2023-06-27 11:51:41 +03:00 committed by GitHub
parent e43924da36
commit d70a1cb0c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 8200 additions and 2739 deletions

View file

@ -22,5 +22,12 @@ export class GoogleDriveOAuth2Api implements ICredentialType {
type: 'hidden', type: 'hidden',
default: scopes.join(' '), default: scopes.join(' '),
}, },
{
displayName:
'Make sure that you have enabled the Google Drive API in the Google Cloud Console. <a href="https://docs.n8n.io/integrations/builtin/credentials/google/oauth-generic/#scopes" target="_blank">More info</a>.',
name: 'notice',
type: 'notice',
default: '',
},
]; ];
} }

File diff suppressed because it is too large Load diff

View file

@ -9,10 +9,10 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow';
import { extractId, googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { extractId, googleApiRequest, googleApiRequestAllItems } from './v1/GenericFunctions';
import moment from 'moment'; import moment from 'moment';
import { fileSearch, folderSearch } from './SearchFunctions'; import { fileSearch, folderSearch } from './v1/SearchFunctions';
export class GoogleDriveTrigger implements INodeType { export class GoogleDriveTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {

View file

@ -0,0 +1,78 @@
import nock from 'nock';
import * as create from '../../../../v2/actions/drive/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports
import * as uuid from 'uuid';
jest.mock('uuid', () => {
const originalModule = jest.requireActual('uuid');
return {
...originalModule,
v4: jest.fn(function () {
return '430c0ca1-2498-472c-9d43-da0163839823';
}),
};
});
describe('test GoogleDriveV2: drive create', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
name: 'newDrive',
options: {
capabilities: {
canComment: true,
canRename: true,
canTrashChildren: true,
},
colorRgb: '#451AD3',
hidden: false,
restrictions: {
driveMembersOnly: true,
},
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await create.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/drives',
{
capabilities: { canComment: true, canRename: true, canTrashChildren: true },
colorRgb: '#451AD3',
hidden: false,
name: 'newDrive',
restrictions: { driveMembersOnly: true },
},
{ requestId: '430c0ca1-2498-472c-9d43-da0163839823' },
);
});
});

View file

@ -0,0 +1,50 @@
import nock from 'nock';
import * as deleteDrive from '../../../../v2/actions/drive/deleteDrive.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive deleteDrive', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'deleteDrive',
driveId: {
__rl: true,
value: 'driveIDxxxxxx',
mode: 'id',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteDrive.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/drives/driveIDxxxxxx',
);
});
});

View file

@ -0,0 +1,55 @@
import nock from 'nock';
import * as get from '../../../../v2/actions/drive/get.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive get', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'get',
driveId: {
__rl: true,
value: 'driveIDxxxxxx',
mode: 'id',
},
options: {
useDomainAdminAccess: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await get.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/drives/driveIDxxxxxx',
{},
{ useDomainAdminAccess: true },
);
});
});

View file

@ -0,0 +1,78 @@
import nock from 'nock';
import * as list from '../../../../v2/actions/drive/list.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
googleApiRequestAllItems: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
};
});
describe('test GoogleDriveV2: drive list', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with limit', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'list',
limit: 20,
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await list.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/drives',
{},
{ pageSize: 20 },
);
});
it('shuold be called with returnAll true', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'list',
returnAll: true,
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await list.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequestAllItems).toBeCalledTimes(1);
expect(transport.googleApiRequestAllItems).toHaveBeenCalledWith(
'GET',
'drives',
'/drive/v3/drives',
{},
{},
);
});
});

View file

@ -0,0 +1,58 @@
import nock from 'nock';
import * as update from '../../../../v2/actions/drive/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive update', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'update',
driveId: {
__rl: true,
value: 'sharedDriveIDxxxxx',
mode: 'id',
},
options: {
colorRgb: '#F4BEBE',
name: 'newName',
restrictions: {
driveMembersOnly: true,
},
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await update.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/drives/sharedDriveIDxxxxx',
{ colorRgb: '#F4BEBE', name: 'newName', restrictions: { driveMembersOnly: true } },
);
});
});

View file

@ -0,0 +1,76 @@
import nock from 'nock';
import * as copy from '../../../../v2/actions/file/copy.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file copy', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'copy',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test01.png',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
name: 'copyImage.png',
sameFolder: false,
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
copyRequiresWriterPermission: true,
description: 'image copy',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await copy.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toBeCalledWith(
'POST',
'/drive/v3/files/fileIDxxxxxx/copy',
{
copyRequiresWriterPermission: true,
description: 'image copy',
name: 'copyImage.png',
parents: ['folderIDxxxxxx'],
},
{
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
});
});

View file

@ -0,0 +1,91 @@
import nock from 'nock';
import * as createFromText from '../../../../v2/actions/file/createFromText.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file createFromText', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'createFromText',
content: 'hello drive!',
name: 'helloDrive.txt',
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
appPropertiesUi: {
appPropertyValues: [
{
key: 'appKey1',
value: 'appValue1',
},
],
},
propertiesUi: {
propertyValues: [
{
key: 'prop1',
value: 'value1',
},
{
key: 'prop2',
value: 'value2',
},
],
},
keepRevisionForever: true,
ocrLanguage: 'en',
useContentAsIndexableText: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await createFromText.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
'\n\t\t\n--XXXXXX\t\t\nContent-Type: application/json; charset=UTF-8\t\t\n\n{"name":"helloDrive.txt","parents":["folderIDxxxxxx"],"mimeType":"text/plain","properties":{"prop1":"value1","prop2":"value2"},"appProperties":{"appKey1":"appValue1"}}\t\t\n--XXXXXX\t\t\nContent-Type: text/plain\t\t\nContent-Transfer-Encoding: base64\t\t\n\nhello drive!\t\t\n--XXXXXX--',
{
corpora: 'allDrives',
includeItemsFromAllDrives: true,
keepRevisionForever: true,
ocrLanguage: 'en',
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
uploadType: 'multipart',
useContentAsIndexableText: true,
},
undefined,
{ headers: { 'Content-Length': 12, 'Content-Type': 'multipart/related; boundary=XXXXXX' } },
);
});
});

View file

@ -0,0 +1,56 @@
import nock from 'nock';
import * as deleteFile from '../../../../v2/actions/file/deleteFile.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file deleteFile', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'deleteFile',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
options: {
deletePermanently: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFile.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{ supportsAllDrives: true },
);
});
});

View file

@ -0,0 +1,64 @@
import nock from 'nock';
import * as download from '../../../../v2/actions/file/download.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file download', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'deleteFile',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
options: {
deletePermanently: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await download.execute.call(fakeExecuteFunction, 0, { json: {} });
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
{},
{ fields: 'mimeType,name', supportsTeamDrives: true },
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
{},
{ alt: 'media' },
undefined,
{ encoding: null, json: false, resolveWithFullResponse: true, useStream: true },
);
});
});

View file

@ -0,0 +1,84 @@
import nock from 'nock';
import * as move from '../../../../v2/actions/file/move.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
parents: ['parentFolderIDxxxxxx'],
};
}
return {};
}),
};
});
describe('test GoogleDriveV2: file move', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'move',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder1',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await move.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{
corpora: 'allDrives',
fields: 'parents',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{
addParents: 'folderIDxxxxxx',
removeParents: 'parentFolderIDxxxxxx',
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
});
});

View file

@ -0,0 +1,74 @@
import nock from 'nock';
import * as share from '../../../../v2/actions/file/share.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file share', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'share',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
permissionsUi: {
permissionsValues: {
role: 'owner',
type: 'user',
emailAddress: 'user@gmail.com',
},
},
options: {
emailMessage: 'some message',
moveToNewOwnersRoot: true,
sendNotificationEmail: true,
transferOwnership: true,
useDomainAdminAccess: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await share.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files/fileIDxxxxxx/permissions',
{ emailAddress: 'user@gmail.com', role: 'owner', type: 'user' },
{
emailMessage: 'some message',
moveToNewOwnersRoot: true,
sendNotificationEmail: true,
supportsAllDrives: true,
transferOwnership: true,
useDomainAdminAccess: true,
},
);
});
});

View file

@ -0,0 +1,66 @@
import nock from 'nock';
import * as update from '../../../../v2/actions/file/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file update', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'update',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
newUpdatedFileName: 'test2.txt',
options: {
keepRevisionForever: true,
ocrLanguage: 'en',
useContentAsIndexableText: true,
fields: ['hasThumbnail', 'starred'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await update.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/fileIDxxxxxx',
{ name: 'test2.txt' },
{
fields: 'hasThumbnail, starred',
keepRevisionForever: true,
ocrLanguage: 'en',
supportsAllDrives: true,
useContentAsIndexableText: true,
},
);
});
});

View file

@ -0,0 +1,96 @@
import nock from 'nock';
import * as upload from '../../../../v2/actions/file/upload.operation';
import * as transport from '../../../../v2/transport';
import * as utils from '../../../../v2/helpers/utils';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'POST') {
return {
headers: { location: 'someLocation' },
};
}
return {};
}),
};
});
jest.mock('../../../../v2/helpers/utils', () => {
const originalModule = jest.requireActual('../../../../v2/helpers/utils');
return {
...originalModule,
getItemBinaryData: jest.fn(async function () {
return {
contentLength: '123',
fileContent: 'Hello Drive!',
originalFilename: 'original.txt',
mimeType: 'text/plain',
};
}),
};
});
describe('test GoogleDriveV2: file upload', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
jest.unmock('../../../../v2/helpers/utils');
});
it('shuold be called with', async () => {
const nodeParameters = {
name: 'newFile.txt',
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
simplifyOutput: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await upload.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
undefined,
{ uploadType: 'resumable' },
undefined,
{ resolveWithFullResponse: true },
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/undefined',
{ mimeType: 'text/plain', name: 'newFile.txt', originalFilename: 'original.txt' },
{
addParents: 'folderIDxxxxxx',
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
expect(utils.getItemBinaryData).toBeCalledTimes(1);
expect(utils.getItemBinaryData).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,119 @@
import nock from 'nock';
import * as search from '../../../../v2/actions/fileFolder/search.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
googleApiRequestAllItems: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
};
});
describe('test GoogleDriveV2: fileFolder search', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('returnAll = false', async () => {
const nodeParameters = {
searchMethod: 'name',
resource: 'fileFolder',
queryString: 'test',
returnAll: false,
limit: 2,
filter: {
whatToSearch: 'files',
fileTypes: ['application/vnd.google-apps.document'],
},
options: {
fields: ['id', 'name', 'starred', 'version'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await search.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toBeCalledWith('GET', '/drive/v3/files', undefined, {
corpora: 'allDrives',
fields: 'nextPageToken, files(id, name, starred, version)',
includeItemsFromAllDrives: true,
pageSize: 2,
q: "name contains 'test' and mimeType != 'application/vnd.google-apps.folder' and trashed = false and (mimeType = 'application/vnd.google-apps.document')",
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
});
});
it('returnAll = true', async () => {
const nodeParameters = {
resource: 'fileFolder',
searchMethod: 'query',
queryString: 'test',
returnAll: true,
filter: {
driveId: {
__rl: true,
value: 'driveID000000123',
mode: 'list',
cachedResultName: 'sharedDrive',
cachedResultUrl: 'https://drive.google.com/drive/folders/driveID000000123',
},
folderId: {
__rl: true,
value: 'folderID000000123',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderID000000123',
},
whatToSearch: 'all',
fileTypes: ['*'],
includeTrashed: true,
},
options: {
fields: ['permissions', 'mimeType'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await search.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequestAllItems).toBeCalledTimes(1);
expect(transport.googleApiRequestAllItems).toBeCalledWith(
'GET',
'files',
'/drive/v3/files',
{},
{
corpora: 'drive',
driveId: 'driveID000000123',
fields: 'nextPageToken, files(permissions, mimeType)',
includeItemsFromAllDrives: true,
q: "test and 'folderID000000123' in parents",
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
});
});

View file

@ -0,0 +1,68 @@
import nock from 'nock';
import * as create from '../../../../v2/actions/folder/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder create', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'folder',
name: 'testFolder 2',
folderId: {
__rl: true,
value: 'root',
mode: 'list',
cachedResultName: 'root',
cachedResultUrl: 'https://drive.google.com/drive',
},
options: {
folderColorRgb: '#167D08',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await create.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files',
{
folderColorRgb: '#167D08',
mimeType: 'application/vnd.google-apps.folder',
name: 'testFolder 2',
parents: ['root'],
},
{
fields: undefined,
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
);
});
});

View file

@ -0,0 +1,80 @@
import nock from 'nock';
import * as deleteFolder from '../../../../v2/actions/folder/deleteFolder.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder deleteFolder', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with PATCH', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'deleteFolder',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFolder.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/folderIDxxxxxx',
{ trashed: true },
{ supportsAllDrives: true },
);
});
it('shuold be called with DELETE', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'deleteFolder',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: { deletePermanently: true },
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFolder.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/files/folderIDxxxxxx',
undefined,
{ supportsAllDrives: true },
);
});
});

View file

@ -0,0 +1,64 @@
import nock from 'nock';
import * as share from '../../../../v2/actions/folder/share.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder share', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'share',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
permissionsUi: {
permissionsValues: {
role: 'reader',
type: 'anyone',
allowFileDiscovery: true,
},
},
options: {
moveToNewOwnersRoot: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await share.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files/folderIDxxxxxx/permissions',
{ allowFileDiscovery: true, role: 'reader', type: 'anyone' },
{ moveToNewOwnersRoot: true, supportsAllDrives: true },
);
});
});

View file

@ -0,0 +1,42 @@
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
import { get } from 'lodash';
import { constructExecutionMetaData, returnJsonArray } from 'n8n-core';
export const driveNode: INode = {
id: '11',
name: 'Google Drive node',
typeVersion: 3,
type: 'n8n-nodes-base.googleDrive',
position: [42, 42],
parameters: {},
};
export const createMockExecuteFunction = (
nodeParameters: IDataObject,
node: INode,
continueOnFail = false,
) => {
const fakeExecuteFunction = {
getNodeParameter(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject | undefined,
options?: IGetNodeParameterOptions | undefined,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
getNode() {
return node;
},
helpers: {
constructExecutionMetaData,
returnJsonArray,
prepareBinaryData: () => {},
httpRequest: () => {},
},
continueOnFail: () => continueOnFail,
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};

View file

@ -0,0 +1,125 @@
import {
prepareQueryString,
setFileProperties,
setUpdateCommonParams,
} from '../../v2/helpers/utils';
describe('test GoogleDriveV2, prepareQueryString', () => {
it('should return id, name', () => {
const fields = undefined;
const result = prepareQueryString(fields);
expect(result).toEqual('id, name');
});
it('should return *', () => {
const fields = ['*'];
const result = prepareQueryString(fields);
expect(result).toEqual('*');
});
it('should return string joined by ,', () => {
const fields = ['id', 'name', 'mimeType'];
const result = prepareQueryString(fields);
expect(result).toEqual('id, name, mimeType');
});
});
describe('test GoogleDriveV2, setFileProperties', () => {
it('should return empty object', () => {
const body = {};
const options = {};
const result = setFileProperties(body, options);
expect(result).toEqual({});
});
it('should return object with properties', () => {
const body = {};
const options = {
propertiesUi: {
propertyValues: [
{
key: 'propertyKey1',
value: 'propertyValue1',
},
{
key: 'propertyKey2',
value: 'propertyValue2',
},
],
},
};
const result = setFileProperties(body, options);
expect(result).toEqual({
properties: {
propertyKey1: 'propertyValue1',
propertyKey2: 'propertyValue2',
},
});
});
it('should return object with appProperties', () => {
const body = {};
const options = {
appPropertiesUi: {
appPropertyValues: [
{
key: 'appPropertyKey1',
value: 'appPropertyValue1',
},
{
key: 'appPropertyKey2',
value: 'appPropertyValue2',
},
],
},
};
const result = setFileProperties(body, options);
expect(result).toEqual({
appProperties: {
appPropertyKey1: 'appPropertyValue1',
appPropertyKey2: 'appPropertyValue2',
},
});
});
});
describe('test GoogleDriveV2, setUpdateCommonParams', () => {
it('should return empty object', () => {
const qs = {};
const options = {};
const result = setUpdateCommonParams(qs, options);
expect(result).toEqual({});
});
it('should return qs with params', () => {
const options = {
useContentAsIndexableText: true,
keepRevisionForever: true,
ocrLanguage: 'en',
trashed: true,
includePermissionsForView: 'published',
};
const qs = setUpdateCommonParams({}, options);
expect(qs).toEqual({
useContentAsIndexableText: true,
keepRevisionForever: true,
ocrLanguage: 'en',
});
});
});

View file

@ -10,7 +10,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../GenericFunctions'; import { getGoogleAccessToken } from '../../GenericFunctions';
export async function googleApiRequest( export async function googleApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IExecuteFunctions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { versionDescription } from './actions/versionDescription';
import { listSearch } from './methods';
import { router } from './actions/router';
export class GoogleDriveV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = { listSearch };
async execute(this: IExecuteFunctions) {
return router.call(this);
}
}

View file

@ -0,0 +1,623 @@
import type { INodeProperties } from 'n8n-workflow';
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:
'https:\\/\\/(?:drive|docs)\\.google\\.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
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:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
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:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
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:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
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:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
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,
},
];

View file

@ -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,
];

View file

@ -0,0 +1,263 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { v4 as uuid } from 'uuid';
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 rename 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;
}

View file

@ -0,0 +1,41 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -0,0 +1,63 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -0,0 +1,103 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -0,0 +1,116 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -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,
];

View file

@ -0,0 +1,136 @@
import type { IExecuteFunctions } from 'n8n-core';
import type {
IDataObject,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
import { setParentFolder } from '../../helpers/utils';
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;
}

View file

@ -0,0 +1,183 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
import { googleApiRequest } from '../../transport';
import { DRIVE } from '../../helpers/interfaces';
import { setFileProperties, setParentFolder, setUpdateCommonParams } from '../../helpers/utils';
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 bodyParameters = setFileProperties(
{
name,
parents: [setParentFolder(folderId, driveId)],
mimeType,
},
options,
);
const boundary = 'XXXXXX';
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 body = `
\n--${boundary}\
\nContent-Type: application/json; charset=UTF-8\
\n\n${JSON.stringify(bodyParameters)}\
\n--${boundary}\
\nContent-Type: text/plain\
\nContent-Transfer-Encoding: base64\
\n\n${content}\
\n--${boundary}--`;
const responseData = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
body,
{
uploadType: 'multipart',
...qs,
},
undefined,
{
headers: {
'Content-Type': `multipart/related; boundary=${boundary}`,
'Content-Length': contentLength,
},
},
);
response = { id: responseData.id };
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject),
{ itemData: { item: i } },
);
return executionData;
}

View file

@ -0,0 +1,67 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -0,0 +1,284 @@
import type { IExecuteFunctions } from 'n8n-core';
import type {
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: 'Binary Property',
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 field to put the binary file data 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',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
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,
resolveWithFullResponse: true,
encoding: null,
json: false,
};
const file = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
{},
{ fields: 'mimeType,name', supportsTeamDrives: 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 },
undefined,
requestOptions,
);
} else {
response = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
{},
{ alt: 'media' },
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;
}

View file

@ -0,0 +1,84 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
import { googleApiRequest } from '../../transport';
import { setParentFolder } from '../../helpers/utils';
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;
}

View file

@ -0,0 +1,64 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, 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;
}

View file

@ -0,0 +1,274 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, 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',
},
undefined,
{
headers: {
'Content-Type': mimeType,
'Content-Length': contentLength,
},
},
);
} else {
const resumableUpload = await googleApiRequest.call(
this,
'PATCH',
`/upload/drive/v3/files/${fileId}`,
undefined,
{ uploadType: 'resumable' },
undefined,
{
resolveWithFullResponse: 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;
}

View file

@ -0,0 +1,189 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
import {
getItemBinaryData,
setFileProperties,
setUpdateCommonParams,
setParentFolder,
} from '../../helpers/utils';
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;
if (Buffer.isBuffer(fileContent)) {
const response = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
fileContent,
{
uploadType: 'media',
},
undefined,
{
headers: {
'Content-Type': mimeType,
'Content-Length': contentLength,
},
},
);
uploadId = response.id;
} else {
const resumableUpload = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
undefined,
{ uploadType: 'resumable' },
undefined,
{
resolveWithFullResponse: true,
},
);
const uploadUrl = resumableUpload.headers.location;
let offset = 0;
for await (const chunk of fileContent) {
const nextOffset = offset + Number(chunk.length);
try {
const response = await this.helpers.httpRequest({
method: 'PUT',
url: uploadUrl,
headers: {
'Content-Length': chunk.length,
'Content-Range': `bytes ${offset}-${nextOffset - 1}/${contentLength}`,
},
body: chunk,
});
uploadId = response?.id;
} catch (error) {
if (error.response?.status !== 308) throw error;
}
offset = nextOffset;
}
}
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;
}

View file

@ -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,
];

View file

@ -0,0 +1,359 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { driveRLC, fileTypesOptions, folderRLC } from '../common.descriptions';
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
import { prepareQueryString, updateDriveScopes } from '../../helpers/utils';
import type { SearchFilter } from '../../helpers/interfaces';
import { DRIVE, RLC_FOLDER_DEFAULT } from '../../helpers/interfaces';
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;
}

View file

@ -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,
];

View file

@ -0,0 +1,109 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { driveRLC, folderRLC } from '../common.descriptions';
import { DRIVE } from '../../helpers/interfaces';
import { setParentFolder } from '../../helpers/utils';
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;
}

View file

@ -0,0 +1,77 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -0,0 +1,64 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { 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;
}

View file

@ -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>;

View file

@ -0,0 +1,55 @@
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { GoogleDriveType } from './node.type';
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 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 this.prepareOutputData(returnData);
}

View file

@ -0,0 +1,90 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import 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: ['main'],
outputs: ['main'],
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,
],
};

View file

@ -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 enum 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',
}

View file

@ -0,0 +1,133 @@
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 = 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';
}
}

View file

@ -0,0 +1 @@
export * as listSearch from './listSearch';

View file

@ -0,0 +1,199 @@
import type {
IDataObject,
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import { googleApiRequest } from '../transport';
import type { SearchFilter } from '../helpers/interfaces';
import { DRIVE, RLC_DRIVE_DEFAULT, RLC_FOLDER_DEFAULT } from '../helpers/interfaces';
import { updateDriveScopes } from '../helpers/utils';
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 = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: rootDefaultDisplayName,
value: RLC_FOLDER_DEFAULT,
url: 'https://drive.google.com/drive',
},
...folders.results,
];
}
return {
results,
paginationToken: folders.paginationToken,
};
}

View file

@ -0,0 +1,111 @@
import type {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IDataObject,
IPollFunctions,
JsonObject,
IHttpRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../../../GenericFunctions';
export async function googleApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | 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.requestOAuth2.call(this, 'googleDriveOAuth2Api', options);
}
} catch (error) {
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
error.statusCode = '401';
}
const apiError = new NodeApiError(
this.getNode(),
{
reason: error.error,
} as JsonObject,
{ httpCode: String(error.statusCode) },
);
if (
apiError.message &&
apiError.description &&
(apiError.message.toLowerCase().includes('bad request') ||
apiError.message.toLowerCase().includes('forbidden') ||
apiError.message.toUpperCase().includes('UNKNOWN ERROR'))
) {
const message = apiError.message;
apiError.message = apiError.description;
apiError.description = message;
}
throw apiError;
}
}
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;
}