import { google } from 'googleapis';
const { Readable } = require('stream');
import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
} from 'n8n-workflow';
import { getAuthenticationClient } from './GoogleApi';
export class GoogleDrive implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Drive',
name: 'googleDrive',
icon: 'file:googleDrive.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Access data on Google Drive',
defaults: {
name: 'Google Drive',
color: '#3f87f2',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'googleApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'File',
value: 'file',
},
{
name: 'Folder',
value: 'folder',
},
],
default: 'file',
description: 'The resource to operate on.',
},
// ----------------------------------
// operations
// ----------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'file',
],
},
},
options: [
{
name: 'Copy',
value: 'copy',
description: 'Copy a file',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a file',
},
{
name: 'Download',
value: 'download',
description: 'Download a file',
},
{
name: 'List',
value: 'list',
description: 'Returns files and folders',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload a file',
},
],
default: 'upload',
description: 'The operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'folder',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a folder',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a folder',
},
],
default: 'create',
description: 'The operation to perform.',
},
// ----------------------------------
// file
// ----------------------------------
// ----------------------------------
// file:copy
// ----------------------------------
{
displayName: 'ID',
name: 'fileId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'copy'
],
resource: [
'file',
],
},
},
description: 'The ID of the file to copy.',
},
// ----------------------------------
// file/folder:delete
// ----------------------------------
{
displayName: 'ID',
name: 'fileId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'delete'
],
resource: [
'file',
'folder',
],
},
},
description: 'The ID of the file/folder to delete.',
},
// ----------------------------------
// file:download
// ----------------------------------
{
displayName: 'File Id',
name: 'fileId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'download'
],
resource: [
'file',
],
},
},
description: 'The ID of the file to download.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
operation: [
'download'
],
resource: [
'file',
],
},
},
description: 'Name of the binary property to which to
write the data of the read file.',
},
// ----------------------------------
// file:list
// ----------------------------------
{
displayName: 'Use Query String',
name: 'useQueryString',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: [
'list'
],
resource: [
'file',
],
},
},
description: 'If a query string should be used to filter results.',
},
{
displayName: 'Query String',
name: 'queryString',
type: 'string',
default: '',
displayOptions: {
show: {
operation: [
'list',
],
useQueryString: [
true,
],
resource: [
'file',
],
},
},
placeholder: 'name contains \'invoice\'',
description: 'Query to use to return only specific files.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'list',
],
resource: [
'file',
],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
default: 100,
description: 'How many files to return.',
},
{
displayName: 'Filters',
name: 'queryFilters',
placeholder: 'Add Filter',
description: 'Filters to use to return only specific files.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
operation: [
'list',
],
useQueryString: [
false,
],
resource: [
'file',
],
},
},
options: [
{
name: 'name',
displayName: 'Name',
values: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Contains',
value: 'contains'
},
{
name: 'Is',
value: 'is'
},
{
name: 'Is Not',
value: 'isNot'
},
],
default: 'contains',
description: 'Operation to perform.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value for operation.',
},
]
},
{
name: 'mimeType',
displayName: 'Mime Type',
values: [
{
displayName: 'Mime Type',
name: 'mimeType',
type: 'options',
options: [
{
name: 'Custom Mime Type',
value: 'custom',
},
{
name: ' 3rd party shortcut',
value: 'application/vnd.google-apps.drive-sdk',
},
{
name: 'Audio',
value: 'application/vnd.google-apps.audio',
},
{
name: 'Google Apps Scripts',
value: 'application/vnd.google-apps.script',
},
{
name: 'Google Docs',
value: 'application/vnd.google-apps.document',
},
{
name: 'Google Drawing',
value: 'application/vnd.google-apps.drawing',
},
{
name: 'Google Drive file',
value: 'application/vnd.google-apps.file',
},
{
name: 'Google Drive folder',
value: 'application/vnd.google-apps.folder',
},
{
name: 'Google Forms',
value: 'application/vnd.google-apps.form',
},
{
name: 'Google Fusion Tables',
value: 'application/vnd.google-apps.fusiontable',
},
{
name: 'Google My Maps',
value: 'application/vnd.google-apps.map',
},
{
name: 'Google Sheets',
value: 'application/vnd.google-apps.spreadsheet',
},
{
name: 'Google Sites',
value: 'application/vnd.google-apps.site',
},
{
name: 'Google Slides',
value: 'application/vnd.google-apps.presentation',
},
{
name: 'Photo',
value: 'application/vnd.google-apps.photo',
},
{
name: 'Unknown',
value: 'application/vnd.google-apps.unknown',
},
{
name: 'Video',
value: 'application/vnd.google-apps.video',
},
],
default: 'application/vnd.google-apps.file',
description: 'The Mime-Type of the files to return.',
},
{
displayName: 'Custom Mime Type',
name: 'customMimeType',
type: 'string',
default: '',
displayOptions: {
show: {
mimeType: [
'custom',
],
},
},
description: 'Custom Mime Type',
},
]
}
],
},
// ----------------------------------
// file:upload
// ----------------------------------
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'upload'
],
resource: [
'file',
],
},
},
placeholder: 'invoice_1.pdf',
description: 'The name the file should be saved as.',
},
{
displayName: 'Parents',
name: 'parents',
type: 'string',
typeOptions: {
multipleValues: true,
},
default: [],
displayOptions: {
show: {
operation: [
'upload',
],
resource: [
'file',
],
},
},
description: 'The IDs of the parent folders which contain the file.',
},
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: [
'upload'
],
resource: [
'file',
],
},
},
description: 'If the data to upload should be taken from binary field.',
},
{
displayName: 'File Content',
name: 'fileContent',
type: 'string',
default: '',
displayOptions: {
show: {
operation: [
'upload'
],
resource: [
'file',
],
binaryData: [
false
],
},
},
placeholder: '',
description: 'The text content of the file to upload.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: [
'upload'
],
resource: [
'file',
],
binaryData: [
true
],
},
},
placeholder: '',
description: 'Name of the binary property which contains
the data for the file to be uploaded.',
},
// ----------------------------------
// folder
// ----------------------------------
// ----------------------------------
// folder:create
// ----------------------------------
{
displayName: 'Folder',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create'
],
resource: [
'folder',
],
},
},
placeholder: 'invoices',
description: 'The name of folder to create.',
},
{
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: 'iconLink',
value: 'iconLink',
},
{
name: 'hasThumbnail',
value: 'hasThumbnail',
},
{
name: 'id',
value: 'id',
},
{
name: 'kind',
value: 'kind',
},
{
name: 'name',
value: 'name',
},
{
name: 'mimeType',
value: 'mimeType',
},
{
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',
},
],
required: true,
default: [],
description: 'The fields to return.',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
displayOptions: {
show: {
'/operation': [
'copy'
],
'/resource': [
'file',
],
},
},
default: '',
placeholder: 'invoice_1.pdf',
description: 'The name the file should be saved as.',
},
{
displayName: 'Parents',
name: 'parents',
type: 'string',
displayOptions: {
show: {
'/operation': [
'copy'
],
'/resource': [
'file',
],
},
},
typeOptions: {
multipleValues: true,
},
default: [],
description: 'The IDs of the parent folders the file should be saved in.',
},
{
displayName: 'Spaces',
name: 'spaces',
type: 'multiOptions',
displayOptions: {
show: {
'/operation': [
'list'
],
'/resource': [
'file',
],
},
},
options: [
{
name: '*',
value: '*',
description: 'All spaces.',
},
{
name: 'appDataFolder',
value: 'appDataFolder',
},
{
name: 'drive',
value: 'drive',
},
{
name: 'photos',
value: 'photos',
},
],
required: true,
default: [],
description: 'The spaces to operate on.',
},
{
displayName: 'Corpora',
name: 'corpora',
type: 'options',
displayOptions: {
show: {
'/operation': [
'list'
],
'/resource': [
'file',
],
},
},
options: [
{
name: 'user',
value: 'user',
description: 'All files in "My Drive" and "Shared with me"',
},
{
name: 'domain',
value: 'domain',
description:"All files shared to the user's domain that are searchable",
},
{
name: 'drive',
value: 'drive',
description: 'All files contained in a single shared drive',
},
{
name: 'allDrives',
value: 'allDrives',
description: 'All drives',
},
],
required: true,
default: [],
description: 'The corpora to operate on.',
},
{
displayName: 'Drive Id',
name: 'driveId',
type: 'string',
default: '',
required: false,
displayOptions: {
show: {
'/operation': [
'list'
],
'/resource': [
'file',
],
corpora: [
'drive'
]
},
},
description: 'ID of the shared drive to search. The driveId parameter must be specified if and only if corpora is set to drive.',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('googleApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.photos.readonly',
];
const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes);
const drive = google.drive({
version: 'v3',
auth: client,
});
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
const options = this.getNodeParameter('options', i) as IDataObject;
let queryFields = 'id, name';
if (options && options.fields) {
const fields = options.fields as string[];
if (fields.includes('*')) {
queryFields = '*';
} else {
queryFields = fields.join(', ');
}
}
if (resource === 'file') {
if (operation === 'copy') {
// ----------------------------------
// copy
// ----------------------------------
const fileId = this.getNodeParameter('fileId', i) as string;
const copyOptions = {
fileId,
fields: queryFields,
requestBody: {} as IDataObject,
};
const optionProperties = ['name', 'parents'];
for (const propertyName of optionProperties) {
if (options[propertyName] !== undefined) {
copyOptions.requestBody[propertyName] = options[propertyName];
}
}
const response = await drive.files.copy(copyOptions);
returnData.push(response.data as IDataObject);
} else if (operation === 'download') {
// ----------------------------------
// download
// ----------------------------------
const fileId = this.getNodeParameter('fileId', i) as string;
const response = await drive.files.get(
{
fileId,
alt: 'media',
},
{
responseType: 'arraybuffer',
},
);
let mimeType: string | undefined;
if (response.headers['content-type']) {
mimeType = response.headers['content-type'];
}
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const data = Buffer.from(response.data as string);
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType);
} else if (operation === 'list') {
// ----------------------------------
// list
// ----------------------------------
let querySpaces = '';
if (options.spaces) {
const spaces = options.spaces as string[];
if (spaces.includes('*')) {
querySpaces = 'appDataFolder, drive, photos';
} else {
querySpaces = spaces.join(', ');
}
}
let queryCorpora = '';
if (options.corpora) {
queryCorpora = (options.corpora as string[]).join(', ');
}
let driveId : string | undefined;
driveId = options.driveId as string;
if (driveId === '') {
driveId = undefined;
}
let queryString = '';
const useQueryString = this.getNodeParameter('useQueryString', i) as boolean;
if (useQueryString === true) {
// Use the user defined query string
queryString = this.getNodeParameter('queryString', i) as string;
} else {
// Build query string out of parameters set by user
const queryFilters = this.getNodeParameter('queryFilters', i) as IDataObject;
const queryFilterFields: string[] = [];
if (queryFilters.name) {
(queryFilters.name as IDataObject[]).forEach(nameFilter => {
let operation = nameFilter.operation;
if (operation === 'is') {
operation = '=';
} else if (operation === 'isNot') {
operation = '!=';
}
queryFilterFields.push(`name ${operation} '${nameFilter.value}'`);
});
queryString += queryFilterFields.join(' or ');
}
queryFilterFields.length = 0;
if (queryFilters.mimeType) {
(queryFilters.mimeType as IDataObject[]).forEach(mimeTypeFilter => {
let mimeType = mimeTypeFilter.mimeType;
if (mimeTypeFilter.mimeType === 'custom') {
mimeType = mimeTypeFilter.customMimeType;
}
queryFilterFields.push(`mimeType = '${mimeType}'`);
});
if (queryFilterFields.length) {
if (queryString !== '') {
queryString += ' and ';
}
queryString += queryFilterFields.join(' or ');
}
}
}
const pageSize = this.getNodeParameter('limit', i) as number;
const res = await drive.files.list({
pageSize,
orderBy: 'modifiedTime',
fields: `nextPageToken, files(${queryFields})`,
spaces: querySpaces,
corpora: queryCorpora,
driveId,
q: queryString,
includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''), // Actually depracated,
supportsAllDrives: (queryCorpora !== '' || driveId !== ''), // see https://developers.google.com/drive/api/v3/reference/files/list
// However until June 2020 still needs to be set, to avoid API errors.
});
const files = res!.data.files;
return [this.helpers.returnJsonArray(files as IDataObject[])];
} else if (operation === 'upload') {
// ----------------------------------
// upload
// ----------------------------------
let mimeType = 'text/plain';
let body;
let originalFilename: string | undefined;
if (this.getNodeParameter('binaryData', i) === true) {
// Is binary file to upload
const item = items[i];
if (item.binary === undefined) {
throw new Error('No binary data exists on item!');
}
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[propertyNameUpload] === undefined) {
throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`);
}
if (item.binary[propertyNameUpload].mimeType) {
mimeType = item.binary[propertyNameUpload].mimeType;
}
if (item.binary[propertyNameUpload].fileName) {
originalFilename = item.binary[propertyNameUpload].fileName;
}
body = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING);
} else {
// Is text file
body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8');
}
const name = this.getNodeParameter('name', i) as string;
const parents = this.getNodeParameter('parents', i) as string[];
const response = await drive.files.create({
requestBody: {
name,
originalFilename,
parents,
},
fields: queryFields,
media: {
mimeType,
body: ((buffer: Buffer) => {
const readableInstanceStream = new Readable({
read() {
this.push(buffer);
this.push(null);
}
});
return readableInstanceStream;
})(body),
},
});
returnData.push(response.data as IDataObject);
}
} else if (resource === 'folder') {
if (operation === 'create') {
// ----------------------------------
// folder:create
// ----------------------------------
const name = this.getNodeParameter('name', i) as string;
const fileMetadata = {
name,
mimeType: 'application/vnd.google-apps.folder'
};
const response = await drive.files.create({
// @ts-ignore
resource: fileMetadata,
fields: queryFields,
});
returnData.push(response.data as IDataObject);
}
}
if (['file', 'folder'].includes(resource)) {
if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const fileId = this.getNodeParameter('fileId', i) as string;
await drive.files.delete({
fileId,
});
// If we are still here it did succeed
returnData.push({
fileId,
success: true,
});
}
} else {
throw new Error(`The resource "${resource}" is not known!`);
}
}
if (resource === 'file' && operation === 'download') {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
// For all other ones does the output items get replaced
return [this.helpers.returnJsonArray(returnData)];
}
}
}