refactor(Google Drive Node): Use node streams for uploading and downloading files (#5017)

* use streams to upload files to google drive

* use streams to download files from google drive

* use resumable uploads api for google drive

* avoid dangling promises, and reduce memory usage in error logging
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-01-04 12:29:56 +01:00 committed by GitHub
parent 8b19fdd5f0
commit 54126b2c87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 229 additions and 163 deletions

View file

@ -1,10 +1,11 @@
import { createReadStream } from 'fs';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { jsonParse } from 'n8n-workflow';
import path from 'path'; import path from 'path';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import { BinaryMetadata, jsonParse } from 'n8n-workflow';
import { BinaryMetadata, IBinaryDataConfig, IBinaryDataManager } from '../Interfaces'; import { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
const PREFIX_METAFILE = 'binarymeta'; const PREFIX_METAFILE = 'binarymeta';
const PREFIX_PERSISTED_METAFILE = 'persistedmeta'; const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
@ -74,6 +75,10 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
return binaryDataId; return binaryDataId;
} }
getBinaryStream(identifier: string, chunkSize?: number): Readable {
return createReadStream(this.getBinaryPath(identifier), { highWaterMark: chunkSize });
}
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> { async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
return this.retrieveFromLocalStorage(identifier); return this.retrieveFromLocalStorage(identifier);
} }

View file

@ -1,10 +1,10 @@
import concatStream from 'concat-stream'; import concatStream from 'concat-stream';
import { readFile, stat } from 'fs/promises'; import { readFile, stat } from 'fs/promises';
import type { IBinaryData, INodeExecutionData } from 'n8n-workflow'; import type { BinaryMetadata, IBinaryData, INodeExecutionData } from 'n8n-workflow';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import { BINARY_ENCODING } from '../Constants'; import { BINARY_ENCODING } from '../Constants';
import type { BinaryMetadata, IBinaryDataConfig, IBinaryDataManager } from '../Interfaces'; import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
import { BinaryDataFileSystem } from './FileSystem'; import { BinaryDataFileSystem } from './FileSystem';
export class BinaryDataManager { export class BinaryDataManager {
@ -88,6 +88,7 @@ export class BinaryDataManager {
const manager = this.managers[this.binaryDataMode]; const manager = this.managers[this.binaryDataMode];
if (manager) { if (manager) {
const identifier = await manager.storeBinaryData(input, executionId); const identifier = await manager.storeBinaryData(input, executionId);
// Add data manager reference id. // Add data manager reference id.
binaryData.id = this.generateBinaryId(identifier); binaryData.id = this.generateBinaryId(identifier);
@ -115,6 +116,15 @@ export class BinaryDataManager {
return binaryData; return binaryData;
} }
getBinaryStream(identifier: string, chunkSize?: number): Readable {
const { mode, id } = this.splitBinaryModeFileId(identifier);
if (this.managers[mode]) {
return this.managers[mode].getBinaryStream(id, chunkSize);
}
throw new Error('Storage mode used to store binary data not available');
}
async retrieveBinaryData(binaryData: IBinaryData): Promise<Buffer> { async retrieveBinaryData(binaryData: IBinaryData): Promise<Buffer> {
if (binaryData.id) { if (binaryData.id) {
return this.retrieveBinaryDataByIdentifier(binaryData.id); return this.retrieveBinaryDataByIdentifier(binaryData.id);

View file

@ -10,6 +10,7 @@ import type {
IPollFunctions as IPollFunctionsBase, IPollFunctions as IPollFunctionsBase,
ITriggerFunctions as ITriggerFunctionsBase, ITriggerFunctions as ITriggerFunctionsBase,
IWebhookFunctions as IWebhookFunctionsBase, IWebhookFunctions as IWebhookFunctionsBase,
BinaryMetadata,
} from 'n8n-workflow'; } from 'n8n-workflow';
// TODO: remove these after removing `n8n-core` dependency from `nodes-bases` // TODO: remove these after removing `n8n-core` dependency from `nodes-bases`
@ -56,12 +57,6 @@ export interface IBinaryDataConfig {
persistedBinaryDataTTL: number; persistedBinaryDataTTL: number;
} }
export interface BinaryMetadata {
fileName?: string;
mimeType?: string;
fileSize: number;
}
export interface IBinaryDataManager { export interface IBinaryDataManager {
init(startPurger: boolean): Promise<void>; init(startPurger: boolean): Promise<void>;
getFileSize(filePath: string): Promise<number>; getFileSize(filePath: string): Promise<number>;
@ -71,6 +66,7 @@ export interface IBinaryDataManager {
storeBinaryData(binaryData: Buffer | Readable, executionId: string): Promise<string>; storeBinaryData(binaryData: Buffer | Readable, executionId: string): Promise<string>;
retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>; retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>;
getBinaryPath(identifier: string): string; getBinaryPath(identifier: string): string;
getBinaryStream(identifier: string, chunkSize?: number): Readable;
markDataForDeletionByExecutionId(executionId: string): Promise<void>; markDataForDeletionByExecutionId(executionId: string): Promise<void>;
deleteMarkedFiles(): Promise<unknown>; deleteMarkedFiles(): Promise<unknown>;
deleteBinaryDataByIdentifier(identifier: string): Promise<void>; deleteBinaryDataByIdentifier(identifier: string): Promise<void>;

View file

@ -66,6 +66,7 @@ import {
IPollFunctions, IPollFunctions,
ITriggerFunctions, ITriggerFunctions,
IWebhookFunctions, IWebhookFunctions,
BinaryMetadata,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Agent } from 'https'; import { Agent } from 'https';
@ -463,7 +464,9 @@ async function parseRequestObject(requestObject: IDataObject) {
} }
} }
if (requestObject.encoding === null) { if (requestObject.useStream) {
axiosConfig.responseType = 'stream';
} else if (requestObject.encoding === null) {
// When downloading files, return an arrayBuffer. // When downloading files, return an arrayBuffer.
axiosConfig.responseType = 'arraybuffer'; axiosConfig.responseType = 'arraybuffer';
} }
@ -519,7 +522,7 @@ function digestAuthAxiosConfig(
const realm: string = authDetails const realm: string = authDetails
.find((el: any) => el[0].toLowerCase().indexOf('realm') > -1)[1] .find((el: any) => el[0].toLowerCase().indexOf('realm') > -1)[1]
.replace(/"/g, ''); .replace(/"/g, '');
// If authDeatials does not have opaque, we should not add it to authorization. // If authDetails does not have opaque, we should not add it to authorization.
const opaqueKV = authDetails.find((el: any) => el[0].toLowerCase().indexOf('opaque') > -1); const opaqueKV = authDetails.find((el: any) => el[0].toLowerCase().indexOf('opaque') > -1);
const opaque: string = opaqueKV ? opaqueKV[1].replace(/"/g, '') : undefined; const opaque: string = opaqueKV ? opaqueKV[1].replace(/"/g, '') : undefined;
const nonce: string = authDetails const nonce: string = authDetails
@ -576,7 +579,7 @@ async function proxyRequestToAxios(
maxBodyLength: Infinity, maxBodyLength: Infinity,
maxContentLength: Infinity, maxContentLength: Infinity,
}; };
let axiosPromise: AxiosPromise;
type ConfigObject = { type ConfigObject = {
auth?: { sendImmediately: boolean }; auth?: { sendImmediately: boolean };
resolveWithFullResponse?: boolean; resolveWithFullResponse?: boolean;
@ -602,107 +605,102 @@ async function proxyRequestToAxios(
// } // }
); );
let requestFn: () => AxiosPromise;
if (configObject.auth?.sendImmediately === false) { if (configObject.auth?.sendImmediately === false) {
// for digest-auth // for digest-auth
const { auth } = axiosConfig; requestFn = async () => {
delete axiosConfig.auth;
// eslint-disable-next-line no-async-promise-executor
axiosPromise = new Promise(async (resolve, reject) => {
try { try {
const result = await axios(axiosConfig); return await axios(axiosConfig);
resolve(result); } catch (error) {
} catch (resp: any) { const { response } = error;
if ( if (response?.status !== 401 || !response.headers['www-authenticate']?.includes('nonce')) {
resp.response === undefined || throw error;
resp.response.status !== 401 ||
!resp.response.headers['www-authenticate']?.includes('nonce')
) {
reject(resp);
} }
axiosConfig = digestAuthAxiosConfig(axiosConfig, resp.response, auth); const { auth } = axiosConfig;
resolve(axios(axiosConfig)); delete axiosConfig.auth;
axiosConfig = digestAuthAxiosConfig(axiosConfig, response, auth);
return await axios(axiosConfig);
} }
}); };
} else { } else {
axiosPromise = axios(axiosConfig); requestFn = async () => axios(axiosConfig);
} }
return new Promise((resolve, reject) => { try {
axiosPromise const response = await requestFn();
.then(async (response) => { if (configObject.resolveWithFullResponse === true) {
if (configObject.resolveWithFullResponse === true) { let body = response.data;
let body = response.data; if (response.data === '') {
if (response.data === '') { if (axiosConfig.responseType === 'arraybuffer') {
if (axiosConfig.responseType === 'arraybuffer') { body = Buffer.alloc(0);
body = Buffer.alloc(0);
} else {
body = undefined;
}
}
await additionalData.hooks?.executeHookFunctions('nodeFetchedData', [workflow.id, node]);
resolve({
body,
headers: response.headers,
statusCode: response.status,
statusMessage: response.statusText,
request: response.request,
});
} else { } else {
let body = response.data; body = undefined;
if (response.data === '') {
if (axiosConfig.responseType === 'arraybuffer') {
body = Buffer.alloc(0);
} else {
body = undefined;
}
}
await additionalData.hooks?.executeHookFunctions('nodeFetchedData', [workflow.id, node]);
resolve(body);
} }
}) }
.catch((error) => { await additionalData.hooks?.executeHookFunctions('nodeFetchedData', [workflow.id, node]);
if (configObject.simple === false && error.response) { return {
if (configObject.resolveWithFullResponse) { body,
resolve({ headers: response.headers,
body: error.response.data, statusCode: response.status,
headers: error.response.headers, statusMessage: response.statusText,
statusCode: error.response.status, request: response.request,
statusMessage: error.response.statusText, };
}); } else {
} else { let body = response.data;
resolve(error.response.data); if (response.data === '') {
} if (axiosConfig.responseType === 'arraybuffer') {
return; body = Buffer.alloc(0);
} else {
body = undefined;
} }
}
await additionalData.hooks?.executeHookFunctions('nodeFetchedData', [workflow.id, node]);
return body;
}
} catch (error) {
const { request, response, isAxiosError, toJSON, config, ...errorData } = error;
if (configObject.simple === false && response) {
if (configObject.resolveWithFullResponse) {
return {
body: response.data,
headers: response.headers,
statusCode: response.status,
statusMessage: response.statusText,
};
} else {
return response.data;
}
}
Logger.debug('Request proxied to Axios failed', { error }); // Axios hydrates the original error with more data. We extract them.
// https://github.com/axios/axios/blob/master/lib/core/enhanceError.js
// Note: `code` is ignored as it's an expected part of the errorData.
if (response) {
Logger.debug('Request proxied to Axios failed', { status: response.status });
let responseData = response.data;
if (Buffer.isBuffer(responseData)) {
responseData = responseData.toString('utf-8');
}
error.message = `${response.status as number} - ${JSON.stringify(responseData)}`;
}
// Axios hydrates the original error with more data. We extract them. error.cause = errorData;
// https://github.com/axios/axios/blob/master/lib/core/enhanceError.js error.error = error.response?.data || errorData;
// Note: `code` is ignored as it's an expected part of the errorData. error.statusCode = error.response?.status;
const { request, response, isAxiosError, toJSON, config, ...errorData } = error; error.options = config || {};
if (response) {
error.message = `${response.status as number} - ${JSON.stringify(response.data)}`;
}
error.cause = errorData; // Remove not needed data and so also remove circular references
error.error = error.response?.data || errorData; error.request = undefined;
error.statusCode = error.response?.status; error.config = undefined;
error.options = config || {}; error.options.adapter = undefined;
error.options.httpsAgent = undefined;
error.options.paramsSerializer = undefined;
error.options.transformRequest = undefined;
error.options.transformResponse = undefined;
error.options.validateStatus = undefined;
// Remove not needed data and so also remove circular references throw error;
error.request = undefined; }
error.config = undefined;
error.options.adapter = undefined;
error.options.httpsAgent = undefined;
error.options.paramsSerializer = undefined;
error.options.transformRequest = undefined;
error.options.transformResponse = undefined;
error.options.validateStatus = undefined;
reject(error);
});
});
} }
function isIterator(obj: unknown): boolean { function isIterator(obj: unknown): boolean {
@ -823,9 +821,22 @@ async function httpRequest(
return result.data; return result.data;
} }
/**
* Returns binary file metadata
*/
export async function getBinaryMetadata(binaryDataId: string): Promise<BinaryMetadata> {
return BinaryDataManager.getInstance().getBinaryMetadata(binaryDataId);
}
/**
* Returns binary file stream for piping
*/
export function getBinaryStream(binaryDataId: string, chunkSize?: number): Readable {
return BinaryDataManager.getInstance().getBinaryStream(binaryDataId, chunkSize);
}
/** /**
* Returns binary data buffer for given item index and property name. * Returns binary data buffer for given item index and property name.
*
*/ */
export async function getBinaryDataBuffer( export async function getBinaryDataBuffer(
inputData: ITaskDataConnections, inputData: ITaskDataConnections,
@ -1989,6 +2000,8 @@ const getRequestHelperFunctions = (
const getBinaryHelperFunctions = ({ const getBinaryHelperFunctions = ({
executionId, executionId,
}: IWorkflowExecuteAdditionalData): BinaryHelperFunctions => ({ }: IWorkflowExecuteAdditionalData): BinaryHelperFunctions => ({
getBinaryStream,
getBinaryMetadata,
prepareBinaryData: async (binaryData, filePath, mimeType) => prepareBinaryData: async (binaryData, filePath, mimeType) =>
prepareBinaryData(binaryData, executionId!, filePath, mimeType), prepareBinaryData(binaryData, executionId!, filePath, mimeType),
setBinaryDataBuffer: async (data, binaryData) => setBinaryDataBuffer: async (data, binaryData) =>

View file

@ -1,4 +1,4 @@
import { IExecuteFunctions } from 'n8n-core'; import { BINARY_ENCODING, IExecuteFunctions } from 'n8n-core';
import { import {
IDataObject, IDataObject,
@ -13,6 +13,9 @@ import {
import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { Readable } from 'stream';
const UPLOAD_CHUNK_SIZE = 256 * 1024;
interface GoogleDriveFilesItem { interface GoogleDriveFilesItem {
id: string; id: string;
@ -2306,6 +2309,7 @@ export class GoogleDrive implements INodeType {
const downloadOptions = this.getNodeParameter('options', i); const downloadOptions = this.getNodeParameter('options', i);
const requestOptions = { const requestOptions = {
useStream: true,
resolveWithFullResponse: true, resolveWithFullResponse: true,
encoding: null, encoding: null,
json: false, json: false,
@ -2316,7 +2320,7 @@ export class GoogleDrive implements INodeType {
'GET', 'GET',
`/drive/v3/files/${fileId}`, `/drive/v3/files/${fileId}`,
{}, {},
{ fields: 'mimeType', supportsTeamDrives: true }, { fields: 'mimeType,name', supportsTeamDrives: true },
); );
let response; let response;
@ -2370,15 +2374,8 @@ export class GoogleDrive implements INodeType {
); );
} }
let mimeType: string | undefined; const mimeType = file.mimeType ?? response.headers['content-type'] ?? undefined;
let fileName: string | undefined = undefined; const fileName = downloadOptions.fileName ?? file.name ?? undefined;
if (response.headers['content-type']) {
mimeType = response.headers['content-type'];
}
if (downloadOptions.fileName) {
fileName = downloadOptions.fileName as string;
}
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: items[i].json, json: items[i].json,
@ -2400,10 +2397,8 @@ export class GoogleDrive implements INodeType {
i, i,
) as string; ) as string;
const data = Buffer.from(response.body as string);
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData( items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
data as unknown as Buffer, response.body as unknown as Readable,
fileName, fileName,
mimeType, mimeType,
); );
@ -2511,9 +2506,11 @@ export class GoogleDrive implements INodeType {
// ---------------------------------- // ----------------------------------
const resolveData = this.getNodeParameter('resolveData', 0); const resolveData = this.getNodeParameter('resolveData', 0);
let mimeType = 'text/plain'; let contentLength: number;
let body; let fileContent: Buffer | Readable;
let originalFilename: string | undefined; let originalFilename: string | undefined;
let mimeType = 'text/plain';
if (this.getNodeParameter('binaryData', i)) { if (this.getNodeParameter('binaryData', i)) {
// Is binary file to upload // Is binary file to upload
const item = items[i]; const item = items[i];
@ -2526,7 +2523,8 @@ export class GoogleDrive implements INodeType {
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string; const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[propertyNameUpload] === undefined) { const binary = item.binary[propertyNameUpload];
if (binary === undefined) {
throw new NodeOperationError( throw new NodeOperationError(
this.getNode(), this.getNode(),
`No binary data property "${propertyNameUpload}" does not exists on item!`, `No binary data property "${propertyNameUpload}" does not exists on item!`,
@ -2534,48 +2532,86 @@ export class GoogleDrive implements INodeType {
); );
} }
if (item.binary[propertyNameUpload].mimeType) { if (binary.id) {
mimeType = item.binary[propertyNameUpload].mimeType; // Stream data in 256KB chunks, and upload the via the resumable upload api
fileContent = this.helpers.getBinaryStream(binary.id, UPLOAD_CHUNK_SIZE);
const metadata = await this.helpers.getBinaryMetadata(binary.id);
contentLength = metadata.fileSize;
originalFilename = metadata.fileName;
if (metadata.mimeType) mimeType = binary.mimeType;
} else {
fileContent = Buffer.from(binary.data, BINARY_ENCODING);
contentLength = fileContent.length;
originalFilename = binary.fileName;
mimeType = binary.mimeType;
} }
if (item.binary[propertyNameUpload].fileName) {
originalFilename = item.binary[propertyNameUpload].fileName;
}
body = await this.helpers.getBinaryDataBuffer(i, propertyNameUpload);
} else { } else {
// Is text file // Is text file
body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); fileContent = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8');
contentLength = fileContent.byteLength;
} }
const name = this.getNodeParameter('name', i) as string; const name = this.getNodeParameter('name', i) as string;
const parents = this.getNodeParameter('parents', i) as string[]; const parents = this.getNodeParameter('parents', i) as string[];
let qs: IDataObject = { let uploadId;
fields: queryFields, if (Buffer.isBuffer(fileContent)) {
uploadType: 'media', const response = await googleApiRequest.call(
}; this,
'POST',
'/upload/drive/v3/files',
fileContent,
{
fields: queryFields,
uploadType: 'media',
},
undefined,
{
headers: {
'Content-Type': mimeType,
'Content-Length': contentLength,
},
encoding: null,
json: false,
},
);
uploadId = JSON.parse(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;
const requestOptions = { let offset = 0;
headers: { for await (const chunk of fileContent) {
'Content-Type': mimeType, const nextOffset = offset + chunk.length;
'Content-Length': body.byteLength, try {
}, const response = await this.helpers.httpRequest({
encoding: null, method: 'PUT',
json: false, 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;
}
}
let response = await googleApiRequest.call( const requestBody = {
this,
'POST',
'/upload/drive/v3/files',
body,
qs,
undefined,
requestOptions,
);
body = {
mimeType, mimeType,
name, name,
originalFilename, originalFilename,
@ -2588,7 +2624,7 @@ export class GoogleDrive implements INodeType {
) as IDataObject[]; ) as IDataObject[];
if (properties.length) { if (properties.length) {
Object.assign(body, { Object.assign(requestBody, {
properties: properties.reduce( properties: properties.reduce(
(obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), (obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }),
{}, {},
@ -2603,7 +2639,7 @@ export class GoogleDrive implements INodeType {
) as IDataObject[]; ) as IDataObject[];
if (properties.length) { if (properties.length) {
Object.assign(body, { Object.assign(requestBody, {
appProperties: appProperties.reduce( appProperties: appProperties.reduce(
(obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), (obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }),
{}, {},
@ -2611,18 +2647,16 @@ export class GoogleDrive implements INodeType {
}); });
} }
qs = { let response = await googleApiRequest.call(
addParents: parents.join(','),
// When set to true shared drives can be used.
supportsAllDrives: true,
};
response = await googleApiRequest.call(
this, this,
'PATCH', 'PATCH',
`/drive/v3/files/${JSON.parse(response).id}`, `/drive/v3/files/${uploadId}`,
body, requestBody,
qs, {
addParents: parents.join(','),
// When set to true shared drives can be used.
supportsAllDrives: true,
},
); );
if (resolveData) { if (resolveData) {

View file

@ -40,10 +40,16 @@ export interface IBinaryData {
fileName?: string; fileName?: string;
directory?: string; directory?: string;
fileExtension?: string; fileExtension?: string;
fileSize?: string; fileSize?: string; // TODO: change this to number and store the actual value
id?: string; id?: string;
} }
export interface BinaryMetadata {
fileName?: string;
mimeType?: string;
fileSize: number;
}
// All properties in this interface except for // All properties in this interface except for
// "includeCredentialsOnRefreshOnBody" will get // "includeCredentialsOnRefreshOnBody" will get
// removed once we add the OAuth2 hooks to the // removed once we add the OAuth2 hooks to the
@ -641,6 +647,9 @@ export interface BinaryHelperFunctions {
): Promise<IBinaryData>; ): Promise<IBinaryData>;
setBinaryDataBuffer(data: IBinaryData, binaryData: Buffer): Promise<IBinaryData>; setBinaryDataBuffer(data: IBinaryData, binaryData: Buffer): Promise<IBinaryData>;
copyBinaryFile(filePath: string, fileName: string, mimeType?: string): Promise<IBinaryData>; copyBinaryFile(filePath: string, fileName: string, mimeType?: string): Promise<IBinaryData>;
getBinaryStream(binaryDataId: string, chunkSize?: number): Readable;
getBinaryMetadata(binaryDataId: string): Promise<BinaryMetadata>;
} }
export interface RequestHelperFunctions { export interface RequestHelperFunctions {
@ -721,7 +730,6 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &
inputData: INodeExecutionData[], inputData: INodeExecutionData[],
options: { itemData: IPairedItemData | IPairedItemData[] }, options: { itemData: IPairedItemData | IPairedItemData[] },
): NodeExecutionWithMetadata[]; ): NodeExecutionWithMetadata[];
getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>; getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>;
}; };
}; };