first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,85 @@
import type { INodeProperties } from 'n8n-workflow';
import * as copy from './copy.operation';
import * as createFromText from './createFromText.operation';
import * as deleteFile from './deleteFile.operation';
import * as download from './download.operation';
import * as move from './move.operation';
import * as share from './share.operation';
import * as update from './update.operation';
import * as upload from './upload.operation';
export { copy, createFromText, deleteFile, download, move, share, update, upload };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['file'],
},
},
options: [
{
name: 'Copy',
value: 'copy',
description: 'Create a copy of an existing file',
action: 'Copy file',
},
{
name: 'Create From Text',
value: 'createFromText',
description: 'Create a file from a provided text',
action: 'Create file from text',
},
{
name: 'Delete',
value: 'deleteFile',
description: 'Permanently delete a file',
action: 'Delete a file',
},
{
name: 'Download',
value: 'download',
description: 'Download a file',
action: 'Download file',
},
{
name: 'Move',
value: 'move',
description: 'Move a file to another folder',
action: 'Move file',
},
{
name: 'Share',
value: 'share',
description: 'Add sharing permissions to a file',
action: 'Share file',
},
{
name: 'Update',
value: 'update',
description: 'Update a file',
action: 'Update file',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload an existing file to Google Drive',
action: 'Upload file',
},
],
default: 'upload',
},
...copy.description,
...deleteFile.description,
...createFromText.description,
...download.description,
...move.description,
...share.description,
...update.description,
...upload.description,
];
@@ -0,0 +1,137 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { setParentFolder } from '../../helpers/utils';
import { googleApiRequest } from '../../transport';
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to copy',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. My File',
description:
'The name of the new file. If not set, “Copy of {original file name}” will be used.',
},
{
displayName: 'Copy In The Same Folder',
name: 'sameFolder',
type: 'boolean',
default: true,
description: 'Whether to copy the file in the same folder as the original file',
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to save the copied file',
displayOptions: { show: { sameFolder: [false] } },
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The folder where to save the copied file',
displayOptions: { show: { sameFolder: [false] } },
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Copy Requires Writer Permission',
name: 'copyRequiresWriterPermission',
type: 'boolean',
default: false,
description:
'Whether the options to copy, print, or download this file, should be disabled for readers and commenters',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'A short description of the file',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['copy'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const file = this.getNodeParameter('fileId', i) as INodeParameterResourceLocator;
const fileId = file.value;
const options = this.getNodeParameter('options', i, {});
let name = this.getNodeParameter('name', i) as string;
name = name ? name : `Copy of ${file.cachedResultName}`;
const copyRequiresWriterPermission = options.copyRequiresWriterPermission || false;
const qs = {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
const parents: string[] = [];
const sameFolder = this.getNodeParameter('sameFolder', i) as boolean;
if (!sameFolder) {
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
parents.push(setParentFolder(folderId, driveId));
}
const body: IDataObject = { copyRequiresWriterPermission, parents, name };
if (options.description) {
body.description = options.description;
}
const response = await googleApiRequest.call(
this,
'POST',
`/drive/v3/files/${fileId}/copy`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}
@@ -0,0 +1,199 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { DRIVE } from '../../helpers/interfaces';
import { setFileProperties, setParentFolder, setUpdateCommonParams } from '../../helpers/utils';
import { googleApiRequest } from '../../transport';
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
import FormData from 'form-data';
const properties: INodeProperties[] = [
{
displayName: 'File Content',
name: 'content',
type: 'string',
default: '',
typeOptions: {
rows: 2,
},
description: 'The text to create the file with',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. My New File',
description:
"The name of the file you want to create. If not specified, 'Untitled' will be used.",
},
{
...driveRLC,
displayName: 'Parent Drive',
required: false,
description: 'The drive where to create the new file',
},
{
...folderRLC,
displayName: 'Parent Folder',
required: false,
description: 'The folder where to create the new file',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
...updateCommonOptions,
{
displayName: 'Convert to Google Document',
name: 'convertToGoogleDocument',
type: 'boolean',
default: false,
description: 'Whether to create a Google Document (instead of the .txt default format)',
hint: 'Google Docs API has to be enabled in the <a href="https://console.developers.google.com/apis/library/docs.googleapis.com" target="_blank">Google API Console</a>.',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['createFromText'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const name = (this.getNodeParameter('name', i) as string) || 'Untitled';
const options = this.getNodeParameter('options', i, {});
const convertToGoogleDocument = (options.convertToGoogleDocument as boolean) || false;
const mimeType = convertToGoogleDocument ? DRIVE.DOCUMENT : 'text/plain';
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
const metadata = {
name,
parents: [setParentFolder(folderId, driveId)],
mimeType,
};
const bodyParameters = setFileProperties(metadata, options);
const qs = setUpdateCommonParams(
{
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
options,
);
let response;
if (convertToGoogleDocument) {
const document = await googleApiRequest.call(
this,
'POST',
'/drive/v3/files',
bodyParameters,
qs,
);
const text = this.getNodeParameter('content', i, '') as string;
const body = {
requests: [
{
insertText: {
text,
endOfSegmentLocation: {
segmentId: '', //empty segment ID signifies the document's body
},
},
},
],
};
const updateResponse = await googleApiRequest.call(
this,
'POST',
'',
body,
undefined,
`https://docs.googleapis.com/v1/documents/${document.id}:batchUpdate`,
);
response = { id: updateResponse.documentId };
} else {
const content = Buffer.from(this.getNodeParameter('content', i, '') as string, 'utf8');
const contentLength = content.byteLength;
const multiPartBody = new FormData();
multiPartBody.append('metadata', JSON.stringify(metadata), {
contentType: 'application/json',
});
multiPartBody.append('data', content, {
contentType: mimeType,
knownLength: contentLength,
});
const uploadData = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
multiPartBody.getBuffer(),
{
uploadType: 'multipart',
supportsAllDrives: true,
},
undefined,
{
headers: {
'Content-Type': `multipart/related; boundary=${multiPartBody.getBoundary()}`,
'Content-Length': multiPartBody.getLengthSync(),
},
},
);
const uploadId = uploadData.id;
qs.addParents = setParentFolder(folderId, driveId);
delete bodyParameters.parents;
const responseData = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${uploadId}`,
bodyParameters,
qs,
);
response = { id: responseData.id };
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject),
{ itemData: { item: i } },
);
return executionData;
}
@@ -0,0 +1,67 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { googleApiRequest } from '../../transport';
import { fileRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to delete',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Delete Permanently',
name: 'deletePermanently',
type: 'boolean',
default: false,
description:
'Whether to delete the file immediately. If false, the file will be moved to the trash.',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['deleteFile'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const deletePermanently = this.getNodeParameter('options.deletePermanently', i, false) as boolean;
const qs = {
supportsAllDrives: true,
};
if (deletePermanently) {
await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${fileId}`, undefined, qs);
} else {
await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${fileId}`, { trashed: true }, qs);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
id: fileId,
success: true,
}),
{ itemData: { item: i } },
);
return executionData;
}
@@ -0,0 +1,285 @@
import type {
IExecuteFunctions,
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { googleApiRequest } from '../../transport';
import { fileRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to download',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Put Output File in Field',
name: 'binaryPropertyName',
type: 'string',
placeholder: 'e.g. data',
default: 'data',
description: 'Use this field name in the following nodes, to use the binary file data',
hint: 'The name of the output binary field to put the file in',
},
{
displayName: 'Google File Conversion',
name: 'googleFileConversion',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
placeholder: 'Add Conversion',
options: [
{
displayName: 'Conversion',
name: 'conversion',
values: [
{
displayName: 'Google Docs',
name: 'docsToFormat',
type: 'options',
options: [
{
name: 'HTML',
value: 'text/html',
},
{
name: 'MS Word Document',
value:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
},
{
name: 'Open Office Document',
value: 'application/vnd.oasis.opendocument.text',
},
{
name: 'PDF',
value: 'application/pdf',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Rich Text (rtf)',
value: 'application/rtf',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Text (txt)',
value: 'text/plain',
},
],
default: 'text/html',
description: 'Format used to export when downloading Google Docs files',
},
{
displayName: 'Google Drawings',
name: 'drawingsToFormat',
type: 'options',
options: [
{
name: 'JPEG',
value: 'image/jpeg',
},
{
name: 'PDF',
value: 'application/pdf',
},
{
name: 'PNG',
value: 'image/png',
},
{
name: 'SVG',
value: 'image/svg+xml',
},
],
default: 'image/jpeg',
description: 'Format used to export when downloading Google Drawings files',
},
{
displayName: 'Google Slides',
name: 'slidesToFormat',
type: 'options',
options: [
{
name: 'MS PowerPoint',
value:
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
},
{
name: 'OpenOffice Presentation',
value: 'application/vnd.oasis.opendocument.presentation',
},
{
name: 'PDF',
value: 'application/pdf',
},
],
default:
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
description: 'Format used to export when downloading Google Slides files',
},
{
displayName: 'Google Sheets',
name: 'sheetsToFormat',
type: 'options',
options: [
{
name: 'CSV',
value: 'text/csv',
},
{
name: 'MS Excel',
value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
{
name: 'Open Office Sheet',
value: 'application/vnd.oasis.opendocument.spreadsheet',
},
{
name: 'PDF',
value: 'application/pdf',
},
],
default: 'text/csv',
description: 'Format used to export when downloading Google Sheets files',
},
],
},
],
},
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
description: 'File name. Ex: data.pdf.',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['download'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
i: number,
item: INodeExecutionData,
): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const downloadOptions = this.getNodeParameter('options', i);
const requestOptions = {
useStream: true,
returnFullResponse: true,
encoding: 'arraybuffer',
json: false,
};
const file = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
{},
{ fields: 'mimeType,name', supportsTeamDrives: true, supportsAllDrives: true },
);
let response;
if (file.mimeType?.includes('vnd.google-apps')) {
const parameterKey = 'options.googleFileConversion.conversion';
const type = file.mimeType.split('.')[2];
let mime;
if (type === 'document') {
mime = this.getNodeParameter(
`${parameterKey}.docsToFormat`,
i,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
) as string;
} else if (type === 'presentation') {
mime = this.getNodeParameter(
`${parameterKey}.slidesToFormat`,
i,
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
) as string;
} else if (type === 'spreadsheet') {
mime = this.getNodeParameter(
`${parameterKey}.sheetsToFormat`,
i,
'application/x-vnd.oasis.opendocument.spreadsheet',
) as string;
} else {
mime = this.getNodeParameter(`${parameterKey}.drawingsToFormat`, i, 'image/jpeg') as string;
}
response = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}/export`,
{},
{ mimeType: mime, supportsAllDrives: true },
undefined,
requestOptions,
);
} else {
response = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
{},
{ alt: 'media', supportsAllDrives: true },
undefined,
requestOptions,
);
}
const mimeType =
(response.headers as IDataObject)?.['content-type'] ?? file.mimeType ?? undefined;
const fileName = downloadOptions.fileName ?? file.name ?? undefined;
const newItem: INodeExecutionData = {
json: item.json,
binary: {},
};
if (item.binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary as IBinaryKeyData, item.binary);
}
item = newItem;
const dataPropertyNameDownload = (downloadOptions.binaryPropertyName as string) || 'data';
item.binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
response.body as Buffer,
fileName as string,
mimeType as string,
);
const executionData = this.helpers.constructExecutionMetaData([item], { itemData: { item: i } });
return executionData;
}
@@ -0,0 +1,89 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { setParentFolder } from '../../helpers/utils';
import { googleApiRequest } from '../../transport';
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to move',
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to move the file',
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The folder where to move the file',
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['move'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
});
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
const qs = {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
const { parents } = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
undefined,
{
...qs,
fields: 'parents',
},
);
const response = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${fileId}`,
undefined,
{
...qs,
addParents: setParentFolder(folderId, driveId),
removeParents: ((parents as string[]) || []).join(','),
},
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}
@@ -0,0 +1,69 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { googleApiRequest } from '../../transport';
import { fileRLC, permissionsOptions, shareOptions } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to share',
},
permissionsOptions,
shareOptions,
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['share'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const permissions = this.getNodeParameter('permissionsUi', i) as IDataObject;
const shareOption = this.getNodeParameter('options', i);
const body: IDataObject = {};
const qs: IDataObject = {
supportsAllDrives: true,
};
if (permissions.permissionsValues) {
Object.assign(body, permissions.permissionsValues);
}
Object.assign(qs, shareOption);
const response = await googleApiRequest.call(
this,
'POST',
`/drive/v3/files/${fileId}/permissions`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}
@@ -0,0 +1,280 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import {
getItemBinaryData,
prepareQueryString,
setFileProperties,
setUpdateCommonParams,
} from '../../helpers/utils';
import { googleApiRequest } from '../../transport';
import { fileRLC, updateCommonOptions } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
displayName: 'File to Update',
description: 'The file to update',
},
{
displayName: 'Change File Content',
name: 'changeFileContent',
type: 'boolean',
default: false,
description: 'Whether to send a new binary data to update the file',
},
{
displayName: 'Input Data Field Name',
name: 'inputDataFieldName',
type: 'string',
placeholder: 'e.g. data',
default: 'data',
hint: 'The name of the input field containing the binary file data to update the file',
description:
'Find the name of input field containing the binary data to update the file in the Input panel on the left, in the Binary tab',
displayOptions: {
show: {
changeFileContent: [true],
},
},
},
{
displayName: 'New Updated File Name',
name: 'newUpdatedFileName',
type: 'string',
default: '',
placeholder: 'e.g. My New File',
description: 'If not specified, the file name will not be changed',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
...updateCommonOptions,
{
displayName: 'Move to Trash',
name: 'trashed',
type: 'boolean',
default: false,
description: 'Whether to move a file to the trash. Only the owner may trash a file.',
},
{
displayName: 'Return Fields',
name: 'fields',
type: 'multiOptions',
options: [
{
name: '[All]',
value: '*',
description: 'All fields',
},
{
name: 'explicitlyTrashed',
value: 'explicitlyTrashed',
},
{
name: 'exportLinks',
value: 'exportLinks',
},
{
name: 'hasThumbnail',
value: 'hasThumbnail',
},
{
name: 'iconLink',
value: 'iconLink',
},
{
name: 'ID',
value: 'id',
},
{
name: 'Kind',
value: 'kind',
},
{
name: 'mimeType',
value: 'mimeType',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Permissions',
value: 'permissions',
},
{
name: 'Shared',
value: 'shared',
},
{
name: 'Spaces',
value: 'spaces',
},
{
name: 'Starred',
value: 'starred',
},
{
name: 'thumbnailLink',
value: 'thumbnailLink',
},
{
name: 'Trashed',
value: 'trashed',
},
{
name: 'Version',
value: 'version',
},
{
name: 'webViewLink',
value: 'webViewLink',
},
],
default: [],
description: 'The fields to return',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const changeFileContent = this.getNodeParameter('changeFileContent', i, false) as boolean;
let mimeType;
// update file binary data
if (changeFileContent) {
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
const binaryData = await getItemBinaryData.call(this, inputDataFieldName, i);
const { contentLength, fileContent } = binaryData;
mimeType = binaryData.mimeType;
if (Buffer.isBuffer(fileContent)) {
await googleApiRequest.call(
this,
'PATCH',
`/upload/drive/v3/files/${fileId}`,
fileContent,
{
uploadType: 'media',
supportsAllDrives: true,
},
undefined,
{
headers: {
'Content-Type': mimeType,
'Content-Length': contentLength,
},
},
);
} else {
const resumableUpload = await googleApiRequest.call(
this,
'PATCH',
`/upload/drive/v3/files/${fileId}`,
undefined,
{ uploadType: 'resumable', supportsAllDrives: true },
undefined,
{
returnFullResponse: true,
},
);
const uploadUrl = resumableUpload.headers.location;
let offset = 0;
for await (const chunk of fileContent) {
const nextOffset = offset + Number(chunk.length);
try {
await this.helpers.httpRequest({
method: 'PUT',
url: uploadUrl,
headers: {
'Content-Length': chunk.length,
'Content-Range': `bytes ${offset}-${nextOffset - 1}/${contentLength}`,
},
body: chunk,
});
} catch (error) {
if (error.response?.status !== 308) {
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
}
}
offset = nextOffset;
}
}
}
const options = this.getNodeParameter('options', i, {});
const qs: IDataObject = setUpdateCommonParams(
{
supportsAllDrives: true,
},
options,
);
if (options.fields) {
const queryFields = prepareQueryString(options.fields as string[]);
qs.fields = queryFields;
}
if (options.trashed) {
qs.trashed = options.trashed;
}
const body: IDataObject = setFileProperties({}, options);
const newUpdatedFileName = this.getNodeParameter('newUpdatedFileName', i, '') as string;
if (newUpdatedFileName) {
body.name = newUpdatedFileName;
}
if (mimeType) {
body.mimeType = mimeType;
}
// update file metadata
const responseData = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${fileId}`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}
@@ -0,0 +1,218 @@
import FormData from 'form-data';
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import {
getItemBinaryData,
setFileProperties,
setUpdateCommonParams,
setParentFolder,
processInChunks,
} from '../../helpers/utils';
import { googleApiRequest } from '../../transport';
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Input Data Field Name',
name: 'inputDataFieldName',
type: 'string',
placeholder: '“e.g. data',
default: 'data',
required: true,
hint: 'The name of the input field containing the binary file data to update the file',
description:
'Find the name of input field containing the binary data to update the file in the Input panel on the left, in the Binary tab',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. My New File',
description: 'If not specified, the original file name will be used',
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to upload the file',
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The folder where to upload the file',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
...updateCommonOptions,
{
displayName: 'Simplify Output',
name: 'simplifyOutput',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of all fields',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['upload'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
const { contentLength, fileContent, originalFilename, mimeType } = await getItemBinaryData.call(
this,
inputDataFieldName,
i,
);
const name = (this.getNodeParameter('name', i) as string) || originalFilename;
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
let uploadId;
const metadata = {
name,
parents: [setParentFolder(folderId, driveId)],
};
if (Buffer.isBuffer(fileContent)) {
const multiPartBody = new FormData();
multiPartBody.append('metadata', JSON.stringify(metadata), {
contentType: 'application/json',
});
multiPartBody.append('data', fileContent, {
contentType: mimeType,
knownLength: contentLength,
});
const response = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
multiPartBody.getBuffer(),
{
uploadType: 'multipart',
supportsAllDrives: true,
},
undefined,
{
headers: {
'Content-Type': `multipart/related; boundary=${multiPartBody.getBoundary()}`,
'Content-Length': multiPartBody.getLengthSync(),
},
},
);
uploadId = response.id;
} else {
const resumableUpload = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
metadata,
{
uploadType: 'resumable',
supportsAllDrives: true,
},
undefined,
{
returnFullResponse: true,
headers: {
'X-Upload-Content-Type': mimeType,
},
},
);
const uploadUrl = resumableUpload.headers.location;
// 2MB chunks, needs to be a multiple of 256kB for Google Drive API
const chunkSizeBytes = 2048 * 1024;
await processInChunks(fileContent, chunkSizeBytes, async (chunk, offset) => {
try {
const response = await this.helpers.httpRequest({
method: 'PUT',
url: uploadUrl,
headers: {
'Content-Length': chunk.length,
'Content-Range': `bytes ${offset}-${offset + chunk.byteLength - 1}/${contentLength}`,
},
body: chunk,
});
uploadId = response?.id;
} catch (error) {
if (error.response?.status !== 308) throw error;
}
});
}
const options = this.getNodeParameter('options', i, {});
const qs = setUpdateCommonParams(
{
addParents: setParentFolder(folderId, driveId),
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
options,
);
if (!options.simplifyOutput) {
qs.fields = '*';
}
const body = setFileProperties(
{
mimeType,
name,
originalFilename,
},
options,
);
const response = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${uploadId}`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}