mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
refactor(core): Reduce memory usage in the Webhook node (#4640)
use file streaming to pass webhook binaries around
This commit is contained in:
parent
602b1e56d6
commit
07e4743a3e
|
@ -1497,18 +1497,22 @@ class App {
|
||||||
// Binary data
|
// Binary data
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
// Returns binary buffer
|
// Download binary
|
||||||
this.app.get(
|
this.app.get(
|
||||||
`/${this.restEndpoint}/data/:path`,
|
`/${this.restEndpoint}/data/:path`,
|
||||||
ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
async (req: express.Request, res: express.Response): Promise<void> => {
|
||||||
// TODO UM: check if this needs permission check for UM
|
// TODO UM: check if this needs permission check for UM
|
||||||
const dataPath = req.params.path;
|
const identifier = req.params.path;
|
||||||
return BinaryDataManager.getInstance()
|
const binaryDataManager = BinaryDataManager.getInstance();
|
||||||
.retrieveBinaryDataByIdentifier(dataPath)
|
const binaryPath = binaryDataManager.getBinaryPath(identifier);
|
||||||
.then((buffer: Buffer) => {
|
const { mimeType, fileName, fileSize } = await binaryDataManager.getBinaryMetadata(
|
||||||
return buffer.toString('base64');
|
identifier,
|
||||||
});
|
);
|
||||||
}),
|
if (mimeType) res.setHeader('Content-Type', mimeType);
|
||||||
|
if (fileName) res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||||
|
res.setHeader('Content-Length', fileSize);
|
||||||
|
res.sendFile(binaryPath);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"n8n-workflow": "~0.126.0",
|
"n8n-workflow": "~0.126.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
|
"pretty-bytes": "^5.6.0",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.7",
|
"request-promise-native": "^1.0.7",
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { promises as fs } from 'fs';
|
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 { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
|
import { BinaryMetadata, IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
|
||||||
|
|
||||||
const PREFIX_METAFILE = 'binarymeta';
|
const PREFIX_METAFILE = 'binarymeta';
|
||||||
const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
|
const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
|
||||||
|
@ -43,17 +44,47 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
|
||||||
.then(() => {});
|
.then(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFileSize(identifier: string): Promise<number> {
|
||||||
|
const stats = await fs.stat(this.getBinaryPath(identifier));
|
||||||
|
return stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyBinaryFile(filePath: string, executionId: string): Promise<string> {
|
||||||
|
const binaryDataId = this.generateFileName(executionId);
|
||||||
|
await this.addBinaryIdToPersistMeta(executionId, binaryDataId);
|
||||||
|
await this.copyFileToLocalStorage(filePath, binaryDataId);
|
||||||
|
return binaryDataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeBinaryMetadata(identifier: string, metadata: BinaryMetadata) {
|
||||||
|
await fs.writeFile(this.getMetadataPath(identifier), JSON.stringify(metadata), {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBinaryMetadata(identifier: string): Promise<BinaryMetadata> {
|
||||||
|
return jsonParse(await fs.readFile(this.getMetadataPath(identifier), { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
async storeBinaryData(binaryBuffer: Buffer, executionId: string): Promise<string> {
|
async storeBinaryData(binaryBuffer: Buffer, executionId: string): Promise<string> {
|
||||||
const binaryDataId = this.generateFileName(executionId);
|
const binaryDataId = this.generateFileName(executionId);
|
||||||
return this.addBinaryIdToPersistMeta(executionId, binaryDataId).then(async () =>
|
await this.addBinaryIdToPersistMeta(executionId, binaryDataId);
|
||||||
this.saveToLocalStorage(binaryBuffer, binaryDataId).then(() => binaryDataId),
|
await this.saveToLocalStorage(binaryBuffer, binaryDataId);
|
||||||
);
|
return binaryDataId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
|
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
|
||||||
return this.retrieveFromLocalStorage(identifier);
|
return this.retrieveFromLocalStorage(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBinaryPath(identifier: string): string {
|
||||||
|
return path.join(this.storagePath, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadataPath(identifier: string): string {
|
||||||
|
return path.join(this.storagePath, `${identifier}.metadata`);
|
||||||
|
}
|
||||||
|
|
||||||
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
|
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
|
||||||
const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000);
|
const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000);
|
||||||
return fs.writeFile(
|
return fs.writeFile(
|
||||||
|
@ -180,7 +211,7 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateFileName(prefix: string): string {
|
private generateFileName(prefix: string): string {
|
||||||
return `${prefix}_${uuid()}`;
|
return [prefix, uuid()].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBinaryDataMetaPath() {
|
private getBinaryDataMetaPath() {
|
||||||
|
@ -196,15 +227,19 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteFromLocalStorage(identifier: string) {
|
private async deleteFromLocalStorage(identifier: string) {
|
||||||
return fs.rm(path.join(this.storagePath, identifier));
|
return fs.rm(this.getBinaryPath(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyFileToLocalStorage(source: string, identifier: string): Promise<void> {
|
||||||
|
await fs.cp(source, this.getBinaryPath(identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveToLocalStorage(data: Buffer, identifier: string) {
|
private async saveToLocalStorage(data: Buffer, identifier: string) {
|
||||||
await fs.writeFile(path.join(this.storagePath, identifier), data);
|
await fs.writeFile(this.getBinaryPath(identifier), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async retrieveFromLocalStorage(identifier: string): Promise<Buffer> {
|
private async retrieveFromLocalStorage(identifier: string): Promise<Buffer> {
|
||||||
const filePath = path.join(this.storagePath, identifier);
|
const filePath = this.getBinaryPath(identifier);
|
||||||
try {
|
try {
|
||||||
return await fs.readFile(filePath);
|
return await fs.readFile(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { IBinaryData, INodeExecutionData } from 'n8n-workflow';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import type { IBinaryData, INodeExecutionData } from 'n8n-workflow';
|
||||||
import { BINARY_ENCODING } from '../Constants';
|
import { BINARY_ENCODING } from '../Constants';
|
||||||
import { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
|
import type { BinaryMetadata, IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
|
||||||
import { BinaryDataFileSystem } from './FileSystem';
|
import { BinaryDataFileSystem } from './FileSystem';
|
||||||
|
import { readFile, stat } from 'fs/promises';
|
||||||
|
|
||||||
export class BinaryDataManager {
|
export class BinaryDataManager {
|
||||||
static instance: BinaryDataManager | undefined;
|
static instance: BinaryDataManager | undefined;
|
||||||
|
@ -43,31 +45,59 @@ export class BinaryDataManager {
|
||||||
return BinaryDataManager.instance;
|
return BinaryDataManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copyBinaryFile(
|
||||||
|
binaryData: IBinaryData,
|
||||||
|
filePath: string,
|
||||||
|
executionId: string,
|
||||||
|
): Promise<IBinaryData> {
|
||||||
|
// If a manager handles this binary, copy over the binary file and return its reference id.
|
||||||
|
const manager = this.managers[this.binaryDataMode];
|
||||||
|
if (manager) {
|
||||||
|
const identifier = await manager.copyBinaryFile(filePath, executionId);
|
||||||
|
// Add data manager reference id.
|
||||||
|
binaryData.id = this.generateBinaryId(identifier);
|
||||||
|
|
||||||
|
// Prevent preserving data in memory if handled by a data manager.
|
||||||
|
binaryData.data = this.binaryDataMode;
|
||||||
|
|
||||||
|
const fileSize = await manager.getFileSize(identifier);
|
||||||
|
binaryData.fileSize = prettyBytes(fileSize);
|
||||||
|
|
||||||
|
await manager.storeBinaryMetadata(identifier, {
|
||||||
|
fileName: binaryData.fileName,
|
||||||
|
mimeType: binaryData.mimeType,
|
||||||
|
fileSize,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { size } = await stat(filePath);
|
||||||
|
binaryData.fileSize = prettyBytes(size);
|
||||||
|
binaryData.data = await readFile(filePath, { encoding: BINARY_ENCODING });
|
||||||
|
}
|
||||||
|
|
||||||
|
return binaryData;
|
||||||
|
}
|
||||||
|
|
||||||
async storeBinaryData(
|
async storeBinaryData(
|
||||||
binaryData: IBinaryData,
|
binaryData: IBinaryData,
|
||||||
binaryBuffer: Buffer,
|
binaryBuffer: Buffer,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
): Promise<IBinaryData> {
|
): Promise<IBinaryData> {
|
||||||
const retBinaryData = binaryData;
|
binaryData.fileSize = prettyBytes(binaryBuffer.length);
|
||||||
|
|
||||||
// If a manager handles this binary, return the binary data with it's reference id.
|
// If a manager handles this binary, return the binary data with its reference id.
|
||||||
if (this.managers[this.binaryDataMode]) {
|
const manager = this.managers[this.binaryDataMode];
|
||||||
return this.managers[this.binaryDataMode]
|
if (manager) {
|
||||||
.storeBinaryData(binaryBuffer, executionId)
|
const identifier = await manager.storeBinaryData(binaryBuffer, executionId);
|
||||||
.then((filename) => {
|
|
||||||
// Add data manager reference id.
|
// Add data manager reference id.
|
||||||
retBinaryData.id = this.generateBinaryId(filename);
|
binaryData.id = this.generateBinaryId(identifier);
|
||||||
|
|
||||||
// Prevent preserving data in memory if handled by a data manager.
|
// Prevent preserving data in memory if handled by a data manager.
|
||||||
retBinaryData.data = this.binaryDataMode;
|
binaryData.data = this.binaryDataMode;
|
||||||
|
} else {
|
||||||
// Short-circuit return to prevent further actions.
|
// Else fallback to storing this data in memory.
|
||||||
return retBinaryData;
|
binaryData.data = binaryBuffer.toString(BINARY_ENCODING);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else fallback to storing this data in memory.
|
|
||||||
retBinaryData.data = binaryBuffer.toString(BINARY_ENCODING);
|
|
||||||
return binaryData;
|
return binaryData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +118,24 @@ export class BinaryDataManager {
|
||||||
throw new Error('Storage mode used to store binary data not available');
|
throw new Error('Storage mode used to store binary data not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBinaryPath(identifier: string): string {
|
||||||
|
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||||
|
if (this.managers[mode]) {
|
||||||
|
return this.managers[mode].getBinaryPath(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Storage mode used to store binary data not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBinaryMetadata(identifier: string): Promise<BinaryMetadata> {
|
||||||
|
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||||
|
if (this.managers[mode]) {
|
||||||
|
return this.managers[mode].getBinaryMetadata(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Storage mode used to store binary data not available');
|
||||||
|
}
|
||||||
|
|
||||||
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
|
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
|
||||||
if (this.managers[this.binaryDataMode]) {
|
if (this.managers[this.binaryDataMode]) {
|
||||||
return this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(executionId);
|
return this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(executionId);
|
||||||
|
|
|
@ -260,6 +260,7 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase {
|
||||||
filePath?: string,
|
filePath?: string,
|
||||||
mimeType?: string,
|
mimeType?: string,
|
||||||
): Promise<IBinaryData>;
|
): Promise<IBinaryData>;
|
||||||
|
copyBinaryFile(filePath: string, fileName: string, mimeType?: string): Promise<IBinaryData>;
|
||||||
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>;
|
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>;
|
||||||
requestWithAuthentication(
|
requestWithAuthentication(
|
||||||
this: IAllExecuteFunctions,
|
this: IAllExecuteFunctions,
|
||||||
|
@ -306,10 +307,21 @@ 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>;
|
||||||
|
copyBinaryFile(filePath: string, executionId: string): Promise<string>;
|
||||||
|
storeBinaryMetadata(identifier: string, metadata: BinaryMetadata): Promise<void>;
|
||||||
|
getBinaryMetadata(identifier: string): Promise<BinaryMetadata>;
|
||||||
storeBinaryData(binaryBuffer: Buffer, executionId: string): Promise<string>;
|
storeBinaryData(binaryBuffer: Buffer, executionId: string): Promise<string>;
|
||||||
retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>;
|
retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>;
|
||||||
|
getBinaryPath(identifier: string): string;
|
||||||
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>;
|
||||||
|
|
|
@ -64,6 +64,7 @@ import {
|
||||||
NodeExecutionWithMetadata,
|
NodeExecutionWithMetadata,
|
||||||
IPairedItemData,
|
IPairedItemData,
|
||||||
deepCopy,
|
deepCopy,
|
||||||
|
BinaryFileType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { Agent } from 'https';
|
import { Agent } from 'https';
|
||||||
|
@ -77,8 +78,8 @@ import FormData from 'form-data';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { OptionsWithUri, OptionsWithUrl, RequestCallback, RequiredUriUrl } from 'request';
|
import { OptionsWithUri, OptionsWithUrl, RequestCallback, RequiredUriUrl } from 'request';
|
||||||
import requestPromise, { RequestPromiseOptions } from 'request-promise-native';
|
import requestPromise, { RequestPromiseOptions } from 'request-promise-native';
|
||||||
import { fromBuffer } from 'file-type';
|
import FileType from 'file-type';
|
||||||
import { lookup } from 'mime-types';
|
import { lookup, extension } from 'mime-types';
|
||||||
import { IncomingHttpHeaders } from 'http';
|
import { IncomingHttpHeaders } from 'http';
|
||||||
import axios, {
|
import axios, {
|
||||||
AxiosError,
|
AxiosError,
|
||||||
|
@ -830,6 +831,13 @@ export async function getBinaryDataBuffer(
|
||||||
return BinaryDataManager.getInstance().retrieveBinaryData(binaryData);
|
return BinaryDataManager.getInstance().retrieveBinaryData(binaryData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileTypeFromMimeType(mimeType: string): BinaryFileType | undefined {
|
||||||
|
if (mimeType.startsWith('image/')) return 'image';
|
||||||
|
if (mimeType.startsWith('video/')) return 'video';
|
||||||
|
if (mimeType.startsWith('text/') || mimeType.startsWith('application/json')) return 'text';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store an incoming IBinaryData & related buffer using the configured binary data manager.
|
* Store an incoming IBinaryData & related buffer using the configured binary data manager.
|
||||||
*
|
*
|
||||||
|
@ -846,10 +854,60 @@ export async function setBinaryDataBuffer(
|
||||||
return BinaryDataManager.getInstance().storeBinaryData(data, binaryData, executionId);
|
return BinaryDataManager.getInstance().storeBinaryData(data, binaryData, executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function copyBinaryFile(
|
||||||
|
executionId: string,
|
||||||
|
filePath: string,
|
||||||
|
fileName: string,
|
||||||
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData> {
|
||||||
|
let fileExtension: string | undefined;
|
||||||
|
if (!mimeType) {
|
||||||
|
// If no mime type is given figure it out
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// Use file path to guess mime type
|
||||||
|
const mimeTypeLookup = lookup(filePath);
|
||||||
|
if (mimeTypeLookup) {
|
||||||
|
mimeType = mimeTypeLookup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mimeType) {
|
||||||
|
// read the first bytes of the file to guess mime type
|
||||||
|
const fileTypeData = await FileType.fromFile(filePath);
|
||||||
|
if (fileTypeData) {
|
||||||
|
mimeType = fileTypeData.mime;
|
||||||
|
fileExtension = fileTypeData.ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mimeType) {
|
||||||
|
// Fall back to text
|
||||||
|
mimeType = 'text/plain';
|
||||||
|
}
|
||||||
|
} else if (!fileExtension) {
|
||||||
|
fileExtension = extension(mimeType) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnData: IBinaryData = {
|
||||||
|
mimeType,
|
||||||
|
fileType: fileTypeFromMimeType(mimeType),
|
||||||
|
fileExtension,
|
||||||
|
data: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
returnData.fileName = fileName;
|
||||||
|
} else if (filePath) {
|
||||||
|
returnData.fileName = path.parse(filePath).base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BinaryDataManager.getInstance().copyBinaryFile(returnData, filePath, executionId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a buffer and converts it into the format n8n uses. It encodes the binary data as
|
* Takes a buffer and converts it into the format n8n uses. It encodes the binary data as
|
||||||
* base64 and adds metadata.
|
* base64 and adds metadata.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
export async function prepareBinaryData(
|
export async function prepareBinaryData(
|
||||||
binaryData: Buffer,
|
binaryData: Buffer,
|
||||||
|
@ -871,7 +929,7 @@ export async function prepareBinaryData(
|
||||||
|
|
||||||
if (!mimeType) {
|
if (!mimeType) {
|
||||||
// Use buffer to guess mime type
|
// Use buffer to guess mime type
|
||||||
const fileTypeData = await fromBuffer(binaryData);
|
const fileTypeData = await FileType.fromBuffer(binaryData);
|
||||||
if (fileTypeData) {
|
if (fileTypeData) {
|
||||||
mimeType = fileTypeData.mime;
|
mimeType = fileTypeData.mime;
|
||||||
fileExtension = fileTypeData.ext;
|
fileExtension = fileTypeData.ext;
|
||||||
|
@ -882,10 +940,13 @@ export async function prepareBinaryData(
|
||||||
// Fall back to text
|
// Fall back to text
|
||||||
mimeType = 'text/plain';
|
mimeType = 'text/plain';
|
||||||
}
|
}
|
||||||
|
} else if (!fileExtension) {
|
||||||
|
fileExtension = extension(mimeType) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const returnData: IBinaryData = {
|
const returnData: IBinaryData = {
|
||||||
mimeType,
|
mimeType,
|
||||||
|
fileType: fileTypeFromMimeType(mimeType),
|
||||||
fileExtension,
|
fileExtension,
|
||||||
data: '',
|
data: '',
|
||||||
};
|
};
|
||||||
|
@ -3076,6 +3137,19 @@ export function getExecuteWebhookFunctions(
|
||||||
async setBinaryDataBuffer(data: IBinaryData, binaryData: Buffer): Promise<IBinaryData> {
|
async setBinaryDataBuffer(data: IBinaryData, binaryData: Buffer): Promise<IBinaryData> {
|
||||||
return setBinaryDataBuffer.call(this, data, binaryData, additionalData.executionId!);
|
return setBinaryDataBuffer.call(this, data, binaryData, additionalData.executionId!);
|
||||||
},
|
},
|
||||||
|
async copyBinaryFile(
|
||||||
|
filePath: string,
|
||||||
|
fileName: string,
|
||||||
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData> {
|
||||||
|
return copyBinaryFile.call(
|
||||||
|
this,
|
||||||
|
additionalData.executionId!,
|
||||||
|
filePath,
|
||||||
|
fileName,
|
||||||
|
mimeType,
|
||||||
|
);
|
||||||
|
},
|
||||||
async prepareBinaryData(
|
async prepareBinaryData(
|
||||||
binaryData: Buffer,
|
binaryData: Buffer,
|
||||||
filePath?: string,
|
filePath?: string,
|
||||||
|
|
|
@ -215,6 +215,7 @@ export interface IRestApi {
|
||||||
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
|
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
|
||||||
getTimezones(): Promise<IDataObject>;
|
getTimezones(): Promise<IDataObject>;
|
||||||
getBinaryBufferString(dataPath: string): Promise<string>;
|
getBinaryBufferString(dataPath: string): Promise<string>;
|
||||||
|
getBinaryUrl(dataPath: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeTranslationHeaders {
|
export interface INodeTranslationHeaders {
|
||||||
|
@ -226,14 +227,6 @@ export interface INodeTranslationHeaders {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBinaryDisplayData {
|
|
||||||
index: number;
|
|
||||||
key: string;
|
|
||||||
node: string;
|
|
||||||
outputIndex: number;
|
|
||||||
runIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IStartRunData {
|
export interface IStartRunData {
|
||||||
workflowData: IWorkflowData;
|
workflowData: IWorkflowData;
|
||||||
startNodes?: string[];
|
startNodes?: string[];
|
||||||
|
|
|
@ -20,10 +20,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import type { IBinaryData, IRunData } from 'n8n-workflow';
|
||||||
IBinaryData,
|
|
||||||
IRunData,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue';
|
import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue';
|
||||||
|
|
||||||
|
@ -44,7 +41,7 @@ export default mixins(
|
||||||
BinaryDataDisplayEmbed,
|
BinaryDataDisplayEmbed,
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'displayData', // IBinaryDisplayData
|
'displayData', // IBinaryData
|
||||||
'windowVisible', // boolean
|
'windowVisible', // boolean
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -67,14 +64,6 @@ export default mixins(
|
||||||
return binaryDataItem;
|
return binaryDataItem;
|
||||||
},
|
},
|
||||||
|
|
||||||
embedClass (): string[] {
|
|
||||||
// @ts-ignore
|
|
||||||
if (this.binaryData! !== null && this.binaryData!.mimeType! !== undefined && (this.binaryData!.mimeType! as string).startsWith('image')) {
|
|
||||||
return ['image'];
|
|
||||||
}
|
|
||||||
return ['other'];
|
|
||||||
},
|
|
||||||
|
|
||||||
workflowRunData (): IRunData | null {
|
workflowRunData (): IRunData | null {
|
||||||
const workflowExecution = this.workflowsStore.getWorkflowExecution;
|
const workflowExecution = this.workflowsStore.getWorkflowExecution;
|
||||||
if (workflowExecution === null) {
|
if (workflowExecution === null) {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
Error loading binary data
|
Error loading binary data
|
||||||
</div>
|
</div>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<video v-if="binaryData.mimeType && binaryData.mimeType.startsWith('video/')" controls autoplay>
|
<video v-if="binaryData.fileType === 'video'" controls autoplay>
|
||||||
<source :src="embedSource" :type="binaryData.mimeType">
|
<source :src="embedSource" :type="binaryData.mimeType">
|
||||||
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
||||||
</video>
|
</video>
|
||||||
|
@ -17,10 +17,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { restApi } from '@/mixins/restApi';
|
import { restApi } from '@/mixins/restApi';
|
||||||
|
import type { IBinaryData } from 'n8n-workflow';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
restApi,
|
restApi,
|
||||||
|
@ -28,7 +27,7 @@ export default mixins(
|
||||||
.extend({
|
.extend({
|
||||||
name: 'BinaryDataDisplayEmbed',
|
name: 'BinaryDataDisplayEmbed',
|
||||||
props: [
|
props: [
|
||||||
'binaryData', // IBinaryDisplayData
|
'binaryData', // IBinaryData
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -38,15 +37,15 @@ export default mixins(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if(!this.binaryData.id) {
|
const id = this.binaryData?.id;
|
||||||
|
if(!id) {
|
||||||
this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + this.binaryData.data;
|
this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + this.binaryData.data;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bufferString = await this.restApi().getBinaryBufferString(this.binaryData!.id!);
|
this.embedSource = this.restApi().getBinaryUrl(id);
|
||||||
this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + bufferString;
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
@ -55,11 +54,8 @@ export default mixins(
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
embedClass(): string[] {
|
embedClass(): string[] {
|
||||||
// @ts-ignore
|
const { fileType } = (this.binaryData || {}) as IBinaryData;
|
||||||
if (this.binaryData! !== null && this.binaryData!.mimeType! !== undefined && (this.binaryData!.mimeType! as string).startsWith('image')) {
|
return [fileType ?? 'other'];
|
||||||
return ['image'];
|
|
||||||
}
|
|
||||||
return ['other'];
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -282,9 +282,13 @@
|
||||||
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.mimeType') }}: </n8n-text></div>
|
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.mimeType') }}: </n8n-text></div>
|
||||||
<div :class="$style.binaryValue">{{binaryData.mimeType}}</div>
|
<div :class="$style.binaryValue">{{binaryData.mimeType}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="binaryData.fileSize">
|
||||||
|
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.fileSize') }}: </n8n-text></div>
|
||||||
|
<div :class="$style.binaryValue">{{binaryData.fileSize}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div :class="$style.binaryButtonContainer">
|
<div :class="$style.binaryButtonContainer">
|
||||||
<n8n-button size="small" :label="$locale.baseText('runData.showBinaryData')" class="binary-data-show-data-button" @click="displayBinaryData(index, key)" />
|
<n8n-button v-if="isViewable(index, key)" size="small" :label="$locale.baseText('runData.showBinaryData')" class="binary-data-show-data-button" @click="displayBinaryData(index, key)" />
|
||||||
<n8n-button v-if="isDownloadable(index, key)" size="small" type="secondary" :label="$locale.baseText('runData.downloadBinaryData')" class="binary-data-show-data-button" @click="downloadBinaryData(index, key)" />
|
<n8n-button v-if="isDownloadable(index, key)" size="small" type="secondary" :label="$locale.baseText('runData.downloadBinaryData')" class="binary-data-show-data-button" @click="downloadBinaryData(index, key)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -341,7 +345,6 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IBinaryDisplayData,
|
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
INodeUpdatePropertiesInformation,
|
INodeUpdatePropertiesInformation,
|
||||||
|
@ -363,7 +366,6 @@ import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
||||||
import WarningTooltip from '@/components/WarningTooltip.vue';
|
import WarningTooltip from '@/components/WarningTooltip.vue';
|
||||||
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
|
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
|
||||||
|
|
||||||
import { copyPaste } from '@/mixins/copyPaste';
|
|
||||||
import { externalHooks } from "@/mixins/externalHooks";
|
import { externalHooks } from "@/mixins/externalHooks";
|
||||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||||
|
@ -385,7 +387,6 @@ export type EnterEditModeArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
copyPaste,
|
|
||||||
externalHooks,
|
externalHooks,
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
nodeHelpers,
|
nodeHelpers,
|
||||||
|
@ -460,7 +461,7 @@ export default mixins(
|
||||||
showData: false,
|
showData: false,
|
||||||
outputIndex: 0,
|
outputIndex: 0,
|
||||||
binaryDataDisplayVisible: false,
|
binaryDataDisplayVisible: false,
|
||||||
binaryDataDisplayData: null as IBinaryDisplayData | null,
|
binaryDataDisplayData: null as IBinaryData | null,
|
||||||
|
|
||||||
MAX_DISPLAY_DATA_SIZE,
|
MAX_DISPLAY_DATA_SIZE,
|
||||||
MAX_DISPLAY_ITEMS_AUTO_ALL,
|
MAX_DISPLAY_ITEMS_AUTO_ALL,
|
||||||
|
@ -1041,23 +1042,26 @@ export default mixins(
|
||||||
this.workflowsStore.setWorkflowExecutionData(null);
|
this.workflowsStore.setWorkflowExecutionData(null);
|
||||||
this.updateNodesExecutionIssues();
|
this.updateNodesExecutionIssues();
|
||||||
},
|
},
|
||||||
|
isViewable (index: number, key: string): boolean {
|
||||||
|
const { fileType }: IBinaryData = this.binaryData[index][key];
|
||||||
|
return !!fileType && ['image', 'video'].includes(fileType);
|
||||||
|
},
|
||||||
isDownloadable (index: number, key: string): boolean {
|
isDownloadable (index: number, key: string): boolean {
|
||||||
const binaryDataItem: IBinaryData = this.binaryData[index][key];
|
const { mimeType, fileName }: IBinaryData = this.binaryData[index][key];
|
||||||
return !!(binaryDataItem.mimeType && binaryDataItem.fileName);
|
return !!(mimeType && fileName);
|
||||||
},
|
},
|
||||||
async downloadBinaryData (index: number, key: string) {
|
async downloadBinaryData (index: number, key: string) {
|
||||||
const binaryDataItem: IBinaryData = this.binaryData[index][key];
|
const { id, data, fileName, fileExtension, mimeType }: IBinaryData = this.binaryData[index][key];
|
||||||
|
|
||||||
let bufferString = 'data:' + binaryDataItem.mimeType + ';base64,';
|
if(id) {
|
||||||
if(binaryDataItem.id) {
|
const url = this.restApi().getBinaryUrl(id);
|
||||||
bufferString += await this.restApi().getBinaryBufferString(binaryDataItem.id);
|
saveAs(url, [fileName, fileExtension].join('.'));
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
bufferString += binaryDataItem.data;
|
const bufferString = 'data:' + mimeType + ';base64,' + data;
|
||||||
|
const blob = await fetch(bufferString).then(d => d.blob());
|
||||||
|
saveAs(blob, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetch(bufferString);
|
|
||||||
const blob = await data.blob();
|
|
||||||
saveAs(blob, binaryDataItem.fileName);
|
|
||||||
},
|
},
|
||||||
displayBinaryData (index: number, key: string) {
|
displayBinaryData (index: number, key: string) {
|
||||||
this.binaryDataDisplayVisible = true;
|
this.binaryDataDisplayVisible = true;
|
||||||
|
|
|
@ -182,6 +182,10 @@ export const restApi = Vue.extend({
|
||||||
getBinaryBufferString: (dataPath: string): Promise<string> => {
|
getBinaryBufferString: (dataPath: string): Promise<string> => {
|
||||||
return self.restApi().makeRestApiRequest('GET', `/data/${dataPath}`);
|
return self.restApi().makeRestApiRequest('GET', `/data/${dataPath}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getBinaryUrl: (dataPath: string): string => {
|
||||||
|
return self.rootStore.getRestApiContext.baseUrl + `/data/${dataPath}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -982,6 +982,7 @@
|
||||||
"runData.items": "Items",
|
"runData.items": "Items",
|
||||||
"runData.json": "JSON",
|
"runData.json": "JSON",
|
||||||
"runData.mimeType": "Mime Type",
|
"runData.mimeType": "Mime Type",
|
||||||
|
"runData.fileSize": "File Size",
|
||||||
"runData.ms": "ms",
|
"runData.ms": "ms",
|
||||||
"runData.noBinaryDataFound": "No binary data found",
|
"runData.noBinaryDataFound": "No binary data found",
|
||||||
"runData.noData": "No data",
|
"runData.noData": "No data",
|
||||||
|
|
|
@ -11,15 +11,16 @@ import {
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import basicAuth from 'basic-auth';
|
|
||||||
|
|
||||||
import { Response } from 'express';
|
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import stream from 'stream';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import basicAuth from 'basic-auth';
|
||||||
|
import type { Response } from 'express';
|
||||||
import formidable from 'formidable';
|
import formidable from 'formidable';
|
||||||
|
|
||||||
import isbot from 'isbot';
|
import isbot from 'isbot';
|
||||||
|
import { file as tmpFile } from 'tmp-promise';
|
||||||
|
|
||||||
|
const pipeline = promisify(stream.pipeline);
|
||||||
|
|
||||||
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
|
@ -673,10 +674,8 @@ export class Wait implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
const mimeType = headers['content-type'] ?? 'application/json';
|
||||||
const mimeType = headers['content-type'] || 'application/json';
|
|
||||||
if (mimeType.includes('multipart/form-data')) {
|
if (mimeType.includes('multipart/form-data')) {
|
||||||
// @ts-ignore
|
|
||||||
const form = new formidable.IncomingForm({ multiples: true });
|
const form = new formidable.IncomingForm({ multiples: true });
|
||||||
|
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
|
@ -715,12 +714,10 @@ export class Wait implements INodeType {
|
||||||
binaryPropertyName = `${options.binaryPropertyName}${count}`;
|
binaryPropertyName = `${options.binaryPropertyName}${count}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileJson = file.toJSON() as unknown as IDataObject;
|
const fileJson = file.toJSON();
|
||||||
const fileContent = await fs.promises.readFile(file.path);
|
returnItem.binary![binaryPropertyName] = await this.helpers.copyBinaryFile(
|
||||||
|
file.path,
|
||||||
returnItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(
|
fileJson.name || fileJson.filename,
|
||||||
Buffer.from(fileContent),
|
|
||||||
fileJson.name as string,
|
|
||||||
fileJson.type as string,
|
fileJson.type as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -735,15 +732,11 @@ export class Wait implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.binaryData === true) {
|
if (options.binaryData === true) {
|
||||||
return new Promise((resolve, _reject) => {
|
const binaryFile = await tmpFile({ prefix: 'n8n-webhook-' });
|
||||||
const binaryPropertyName = options.binaryPropertyName || 'data';
|
|
||||||
const data: Buffer[] = [];
|
|
||||||
|
|
||||||
req.on('data', (chunk) => {
|
try {
|
||||||
data.push(chunk);
|
await pipeline(req, fs.createWriteStream(binaryFile.path));
|
||||||
});
|
|
||||||
|
|
||||||
req.on('end', async () => {
|
|
||||||
const returnItem: INodeExecutionData = {
|
const returnItem: INodeExecutionData = {
|
||||||
binary: {},
|
binary: {},
|
||||||
json: {
|
json: {
|
||||||
|
@ -754,19 +747,20 @@ export class Wait implements INodeType {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
returnItem.binary![binaryPropertyName as string] = await this.helpers.prepareBinaryData(
|
const binaryPropertyName = (options.binaryPropertyName || 'data') as string;
|
||||||
Buffer.concat(data),
|
returnItem.binary![binaryPropertyName] = await this.helpers.copyBinaryFile(
|
||||||
|
binaryFile.path,
|
||||||
|
mimeType,
|
||||||
);
|
);
|
||||||
|
|
||||||
return resolve({
|
return {
|
||||||
workflowData: [[returnItem]],
|
workflowData: [[returnItem]],
|
||||||
});
|
};
|
||||||
});
|
} catch (error) {
|
||||||
|
|
||||||
req.on('error', (error) => {
|
|
||||||
throw new NodeOperationError(this.getNode(), error);
|
throw new NodeOperationError(this.getNode(), error);
|
||||||
});
|
} finally {
|
||||||
});
|
await binaryFile.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: INodeExecutionData = {
|
const response: INodeExecutionData = {
|
||||||
|
|
|
@ -10,15 +10,16 @@ import {
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import basicAuth from 'basic-auth';
|
|
||||||
|
|
||||||
import { Response } from 'express';
|
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import stream from 'stream';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import basicAuth from 'basic-auth';
|
||||||
|
import type { Response } from 'express';
|
||||||
import formidable from 'formidable';
|
import formidable from 'formidable';
|
||||||
|
|
||||||
import isbot from 'isbot';
|
import isbot from 'isbot';
|
||||||
|
import { file as tmpFile } from 'tmp-promise';
|
||||||
|
|
||||||
|
const pipeline = promisify(stream.pipeline);
|
||||||
|
|
||||||
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
|
@ -485,10 +486,8 @@ export class Webhook implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
const mimeType = headers['content-type'] ?? 'application/json';
|
||||||
const mimeType = headers['content-type'] || 'application/json';
|
|
||||||
if (mimeType.includes('multipart/form-data')) {
|
if (mimeType.includes('multipart/form-data')) {
|
||||||
// @ts-ignore
|
|
||||||
const form = new formidable.IncomingForm({ multiples: true });
|
const form = new formidable.IncomingForm({ multiples: true });
|
||||||
|
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
|
@ -527,12 +526,10 @@ export class Webhook implements INodeType {
|
||||||
binaryPropertyName = `${options.binaryPropertyName}${count}`;
|
binaryPropertyName = `${options.binaryPropertyName}${count}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileJson = file.toJSON() as unknown as IDataObject;
|
const fileJson = file.toJSON();
|
||||||
const fileContent = await fs.promises.readFile(file.path);
|
returnItem.binary![binaryPropertyName] = await this.helpers.copyBinaryFile(
|
||||||
|
file.path,
|
||||||
returnItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(
|
fileJson.name || fileJson.filename,
|
||||||
Buffer.from(fileContent),
|
|
||||||
fileJson.name as string,
|
|
||||||
fileJson.type as string,
|
fileJson.type as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -547,15 +544,11 @@ export class Webhook implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.binaryData === true) {
|
if (options.binaryData === true) {
|
||||||
return new Promise((resolve, _reject) => {
|
const binaryFile = await tmpFile({ prefix: 'n8n-webhook-' });
|
||||||
const binaryPropertyName = options.binaryPropertyName || 'data';
|
|
||||||
const data: Buffer[] = [];
|
|
||||||
|
|
||||||
req.on('data', (chunk) => {
|
try {
|
||||||
data.push(chunk);
|
await pipeline(req, fs.createWriteStream(binaryFile.path));
|
||||||
});
|
|
||||||
|
|
||||||
req.on('end', async () => {
|
|
||||||
const returnItem: INodeExecutionData = {
|
const returnItem: INodeExecutionData = {
|
||||||
binary: {},
|
binary: {},
|
||||||
json: {
|
json: {
|
||||||
|
@ -566,19 +559,20 @@ export class Webhook implements INodeType {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
returnItem.binary![binaryPropertyName as string] = await this.helpers.prepareBinaryData(
|
const binaryPropertyName = (options.binaryPropertyName || 'data') as string;
|
||||||
Buffer.concat(data),
|
returnItem.binary![binaryPropertyName] = await this.helpers.copyBinaryFile(
|
||||||
|
binaryFile.path,
|
||||||
|
mimeType,
|
||||||
);
|
);
|
||||||
|
|
||||||
return resolve({
|
return {
|
||||||
workflowData: [[returnItem]],
|
workflowData: [[returnItem]],
|
||||||
});
|
};
|
||||||
});
|
} catch (error) {
|
||||||
|
|
||||||
req.on('error', (error) => {
|
|
||||||
throw new NodeOperationError(this.getNode(), error);
|
throw new NodeOperationError(this.getNode(), error);
|
||||||
});
|
} finally {
|
||||||
});
|
await binaryFile.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: INodeExecutionData = {
|
const response: INodeExecutionData = {
|
||||||
|
|
|
@ -27,13 +27,16 @@ export type IAllExecuteFunctions =
|
||||||
| ITriggerFunctions
|
| ITriggerFunctions
|
||||||
| IWebhookFunctions;
|
| IWebhookFunctions;
|
||||||
|
|
||||||
|
export type BinaryFileType = 'text' | 'image' | 'video';
|
||||||
export interface IBinaryData {
|
export interface IBinaryData {
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
data: string;
|
data: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
|
fileType?: BinaryFileType;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
fileExtension?: string;
|
fileExtension?: string;
|
||||||
|
fileSize?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -349,6 +349,7 @@ importers:
|
||||||
n8n-workflow: ~0.126.0
|
n8n-workflow: ~0.126.0
|
||||||
oauth-1.0a: ^2.2.6
|
oauth-1.0a: ^2.2.6
|
||||||
p-cancelable: ^2.0.0
|
p-cancelable: ^2.0.0
|
||||||
|
pretty-bytes: ^5.6.0
|
||||||
qs: ^6.10.1
|
qs: ^6.10.1
|
||||||
request: ^2.88.2
|
request: ^2.88.2
|
||||||
request-promise-native: ^1.0.7
|
request-promise-native: ^1.0.7
|
||||||
|
@ -367,6 +368,7 @@ importers:
|
||||||
n8n-workflow: link:../workflow
|
n8n-workflow: link:../workflow
|
||||||
oauth-1.0a: 2.2.6
|
oauth-1.0a: 2.2.6
|
||||||
p-cancelable: 2.1.1
|
p-cancelable: 2.1.1
|
||||||
|
pretty-bytes: 5.6.0
|
||||||
qs: 6.11.0
|
qs: 6.11.0
|
||||||
request: 2.88.2
|
request: 2.88.2
|
||||||
request-promise-native: 1.0.9_request@2.88.2
|
request-promise-native: 1.0.9_request@2.88.2
|
||||||
|
@ -17465,7 +17467,6 @@ packages:
|
||||||
/pretty-bytes/5.6.0:
|
/pretty-bytes/5.6.0:
|
||||||
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pretty-error/2.1.2:
|
/pretty-error/2.1.2:
|
||||||
resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==}
|
resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==}
|
||||||
|
|
Loading…
Reference in a new issue