mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(core): Generalize binary data manager interface (no-changelog) (#7164)
Depends on: #7092 | Story: [PAY-768](https://linear.app/n8n/issue/PAY-768) This PR: - Generalizes the `IBinaryDataManager` interface. - Adjusts `Filesystem.ts` to satisfy the interface. - Sets up an S3 client stub to be filled in in the next PR. - Turns `BinaryDataManager` into an injectable service. - Adjusts the config schema and adds new validators. Note that the PR looks large but all the main changes are in `packages/core/src/binaryData`. Out of scope: - `BinaryDataManager` (now `BinaryDataService`) and `Filesystem.ts` (now `fs.client.ts`) were slightly refactored for maintainability, but fully overhauling them is **not** the focus of this PR, which is meant to clear the way for the S3 implementation. Future improvements for these two should include setting up a backwards-compatible dir structure that makes it easier to locate binary data files to delete, removing duplication, simplifying cloning methods, using integers for binary data size instead of `prettyBytes()`, writing tests for existing binary data logic, etc. --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -26,7 +26,7 @@ import type { RequestOptions } from 'oauth-1.0a';
|
|||||||
import clientOAuth1 from 'oauth-1.0a';
|
import clientOAuth1 from 'oauth-1.0a';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BinaryDataManager,
|
BinaryDataService,
|
||||||
Credentials,
|
Credentials,
|
||||||
LoadMappingOptions,
|
LoadMappingOptions,
|
||||||
LoadNodeParameterOptions,
|
LoadNodeParameterOptions,
|
||||||
@@ -202,6 +202,8 @@ export class Server extends AbstractServer {
|
|||||||
|
|
||||||
push: Push;
|
push: Push;
|
||||||
|
|
||||||
|
binaryDataService: BinaryDataService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('main');
|
super('main');
|
||||||
|
|
||||||
@@ -361,6 +363,7 @@ export class Server extends AbstractServer {
|
|||||||
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
||||||
|
|
||||||
this.push = Container.get(Push);
|
this.push = Container.get(Push);
|
||||||
|
this.binaryDataService = Container.get(BinaryDataService);
|
||||||
|
|
||||||
await super.start();
|
await super.start();
|
||||||
LoggerProxy.debug(`Server ID: ${this.uniqueInstanceId}`);
|
LoggerProxy.debug(`Server ID: ${this.uniqueInstanceId}`);
|
||||||
@@ -1427,13 +1430,12 @@ export class Server extends AbstractServer {
|
|||||||
async (req: BinaryDataRequest, res: express.Response): Promise<void> => {
|
async (req: BinaryDataRequest, 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 identifier = req.params.path;
|
const identifier = req.params.path;
|
||||||
const binaryDataManager = BinaryDataManager.getInstance();
|
|
||||||
try {
|
try {
|
||||||
const binaryPath = binaryDataManager.getBinaryPath(identifier);
|
const binaryPath = this.binaryDataService.getPath(identifier);
|
||||||
let { mode, fileName, mimeType } = req.query;
|
let { mode, fileName, mimeType } = req.query;
|
||||||
if (!fileName || !mimeType) {
|
if (!fileName || !mimeType) {
|
||||||
try {
|
try {
|
||||||
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
|
const metadata = await this.binaryDataService.getMetadata(identifier);
|
||||||
fileName = metadata.fileName;
|
fileName = metadata.fileName;
|
||||||
mimeType = metadata.mimeType;
|
mimeType = metadata.mimeType;
|
||||||
res.setHeader('Content-Length', metadata.fileSize);
|
res.setHeader('Content-Length', metadata.fileSize);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import stream from 'stream';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import formidable from 'formidable';
|
import formidable from 'formidable';
|
||||||
|
|
||||||
import { BinaryDataManager, NodeExecuteFunctions } from 'n8n-core';
|
import { BinaryDataService, NodeExecuteFunctions } from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
@@ -514,7 +514,7 @@ export async function executeWebhook(
|
|||||||
const binaryData = (response.body as IDataObject)?.binaryData as IBinaryData;
|
const binaryData = (response.body as IDataObject)?.binaryData as IBinaryData;
|
||||||
if (binaryData?.id) {
|
if (binaryData?.id) {
|
||||||
res.header(response.headers);
|
res.header(response.headers);
|
||||||
const stream = BinaryDataManager.getInstance().getBinaryStream(binaryData.id);
|
const stream = Container.get(BinaryDataService).getAsStream(binaryData.id);
|
||||||
void pipeline(stream, res).then(() =>
|
void pipeline(stream, res).then(() =>
|
||||||
responseCallback(null, { noWebhookResponse: true }),
|
responseCallback(null, { noWebhookResponse: true }),
|
||||||
);
|
);
|
||||||
@@ -734,7 +734,7 @@ export async function executeWebhook(
|
|||||||
// Send the webhook response manually
|
// Send the webhook response manually
|
||||||
res.setHeader('Content-Type', binaryData.mimeType);
|
res.setHeader('Content-Type', binaryData.mimeType);
|
||||||
if (binaryData.id) {
|
if (binaryData.id) {
|
||||||
const stream = BinaryDataManager.getInstance().getBinaryStream(binaryData.id);
|
const stream = Container.get(BinaryDataService).getAsStream(binaryData.id);
|
||||||
await pipeline(stream, res);
|
await pipeline(stream, res);
|
||||||
} else {
|
} else {
|
||||||
res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
|
res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ setDefaultResultOrder('ipv4first');
|
|||||||
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import type { IProcessMessage } from 'n8n-core';
|
import type { IProcessMessage } from 'n8n-core';
|
||||||
import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core';
|
import { BinaryDataService, UserSettings, WorkflowExecute } from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
@@ -124,7 +124,7 @@ class WorkflowRunnerProcess {
|
|||||||
await Container.get(InternalHooks).init(instanceId);
|
await Container.get(InternalHooks).init(instanceId);
|
||||||
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
await BinaryDataManager.init(binaryDataConfig);
|
await Container.get(BinaryDataService).init(binaryDataConfig);
|
||||||
|
|
||||||
const license = Container.get(License);
|
const license = Container.get(License);
|
||||||
await license.init(instanceId);
|
await license.init(instanceId);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ExitError } from '@oclif/errors';
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
|
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
|
||||||
import type { IUserSettings } from 'n8n-core';
|
import type { IUserSettings } from 'n8n-core';
|
||||||
import { BinaryDataManager, UserSettings } from 'n8n-core';
|
import { BinaryDataService, UserSettings } from 'n8n-core';
|
||||||
import type { AbstractServer } from '@/AbstractServer';
|
import type { AbstractServer } from '@/AbstractServer';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
@@ -105,9 +105,9 @@ export abstract class BaseCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initBinaryManager() {
|
async initBinaryDataService() {
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
await BinaryDataManager.init(binaryDataConfig, true);
|
await Container.get(BinaryDataService).init(binaryDataConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initExternalHooks() {
|
async initExternalHooks() {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class Execute extends BaseCommand {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await super.init();
|
await super.init();
|
||||||
await this.initBinaryManager();
|
await this.initBinaryDataService();
|
||||||
await this.initExternalHooks();
|
await this.initExternalHooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class ExecuteBatch extends BaseCommand {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await super.init();
|
await super.init();
|
||||||
await this.initBinaryManager();
|
await this.initBinaryDataService();
|
||||||
await this.initExternalHooks();
|
await this.initExternalHooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export class Start extends BaseCommand {
|
|||||||
this.activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
|
this.activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
|
||||||
|
|
||||||
await this.initLicense('main');
|
await this.initLicense('main');
|
||||||
await this.initBinaryManager();
|
await this.initBinaryDataService();
|
||||||
await this.initExternalHooks();
|
await this.initExternalHooks();
|
||||||
await this.initExternalSecrets();
|
await this.initExternalSecrets();
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export class Webhook extends BaseCommand {
|
|||||||
await super.init();
|
await super.init();
|
||||||
|
|
||||||
await this.initLicense('webhook');
|
await this.initLicense('webhook');
|
||||||
await this.initBinaryManager();
|
await this.initBinaryDataService();
|
||||||
await this.initExternalHooks();
|
await this.initExternalHooks();
|
||||||
await this.initExternalSecrets();
|
await this.initExternalSecrets();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export class Worker extends BaseCommand {
|
|||||||
this.logger.debug('Starting n8n worker...');
|
this.logger.debug('Starting n8n worker...');
|
||||||
|
|
||||||
await this.initLicense('worker');
|
await this.initLicense('worker');
|
||||||
await this.initBinaryManager();
|
await this.initBinaryDataService();
|
||||||
await this.initExternalHooks();
|
await this.initExternalHooks();
|
||||||
await this.initExternalSecrets();
|
await this.initExternalSecrets();
|
||||||
await this.initEventBus();
|
await this.initEventBus();
|
||||||
|
|||||||
@@ -2,28 +2,21 @@ import path from 'path';
|
|||||||
import convict from 'convict';
|
import convict from 'convict';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { UserSettings } from 'n8n-core';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
import { ensureStringArray } from './utils';
|
||||||
|
|
||||||
convict.addFormat({
|
convict.addFormat({
|
||||||
name: 'nodes-list',
|
name: 'json-string-array',
|
||||||
// @ts-ignore
|
coerce: (rawStr: string) =>
|
||||||
validate(values: string[], { env }: { env: string }): void {
|
jsonParse<string[]>(rawStr, {
|
||||||
try {
|
errorMessage: `Expected this value "${rawStr}" to be valid JSON`,
|
||||||
if (!Array.isArray(values)) {
|
}),
|
||||||
throw new Error();
|
validate: ensureStringArray,
|
||||||
}
|
});
|
||||||
|
|
||||||
for (const value of values) {
|
convict.addFormat({
|
||||||
if (typeof value !== 'string') {
|
name: 'comma-separated-list',
|
||||||
throw new Error();
|
coerce: (rawStr: string) => rawStr.split(','),
|
||||||
}
|
validate: ensureStringArray,
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new TypeError(`${env} is not a valid Array of strings.`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
coerce(rawValue: string): string[] {
|
|
||||||
return jsonParse(rawValue, { errorMessage: 'nodes-list needs to be valid JSON' });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schema = {
|
export const schema = {
|
||||||
@@ -788,13 +781,13 @@ export const schema = {
|
|||||||
nodes: {
|
nodes: {
|
||||||
include: {
|
include: {
|
||||||
doc: 'Nodes to load',
|
doc: 'Nodes to load',
|
||||||
format: 'nodes-list',
|
format: 'json-string-array',
|
||||||
default: undefined,
|
default: undefined,
|
||||||
env: 'NODES_INCLUDE',
|
env: 'NODES_INCLUDE',
|
||||||
},
|
},
|
||||||
exclude: {
|
exclude: {
|
||||||
doc: 'Nodes not to load',
|
doc: 'Nodes not to load',
|
||||||
format: 'nodes-list',
|
format: 'json-string-array',
|
||||||
default: undefined,
|
default: undefined,
|
||||||
env: 'NODES_EXCLUDE',
|
env: 'NODES_EXCLUDE',
|
||||||
},
|
},
|
||||||
@@ -902,7 +895,7 @@ export const schema = {
|
|||||||
|
|
||||||
binaryDataManager: {
|
binaryDataManager: {
|
||||||
availableModes: {
|
availableModes: {
|
||||||
format: String,
|
format: 'comma-separated-list',
|
||||||
default: 'filesystem',
|
default: 'filesystem',
|
||||||
env: 'N8N_AVAILABLE_BINARY_DATA_MODES',
|
env: 'N8N_AVAILABLE_BINARY_DATA_MODES',
|
||||||
doc: 'Available modes of binary data storage, as comma separated strings',
|
doc: 'Available modes of binary data storage, as comma separated strings',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
import type { IBinaryDataConfig } from 'n8n-core';
|
import type { BinaryData } from 'n8n-core';
|
||||||
import type { schema } from './schema';
|
import type { schema } from './schema';
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
@@ -76,7 +76,7 @@ type ToReturnType<T extends ConfigOptionPath> = T extends NumericPath
|
|||||||
|
|
||||||
type ExceptionPaths = {
|
type ExceptionPaths = {
|
||||||
'queue.bull.redis': object;
|
'queue.bull.redis': object;
|
||||||
binaryDataManager: IBinaryDataConfig;
|
binaryDataManager: BinaryData.Config;
|
||||||
'nodes.exclude': string[] | undefined;
|
'nodes.exclude': string[] | undefined;
|
||||||
'nodes.include': string[] | undefined;
|
'nodes.include': string[] | undefined;
|
||||||
'userManagement.isInstanceOwnerSetUp': boolean;
|
'userManagement.isInstanceOwnerSetUp': boolean;
|
||||||
|
|||||||
17
packages/cli/src/config/utils.ts
Normal file
17
packages/cli/src/config/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SchemaObj } from 'convict';
|
||||||
|
|
||||||
|
class NotStringArrayError extends Error {
|
||||||
|
constructor(env: string) {
|
||||||
|
super(`${env} is not a string array.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ensureStringArray = (values: string[], { env }: SchemaObj<string>) => {
|
||||||
|
if (!env) throw new Error(`Missing env: ${env}`);
|
||||||
|
|
||||||
|
if (!Array.isArray(values)) throw new NotStringArrayError(env);
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== 'string') throw new NotStringArrayError(env);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
import { parse, stringify } from 'flatted';
|
import { parse, stringify } from 'flatted';
|
||||||
import { LoggerProxy as Logger } from 'n8n-workflow';
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
import type { IExecutionsSummary, IRunExecutionData } from 'n8n-workflow';
|
import type { IExecutionsSummary, IRunExecutionData } from 'n8n-workflow';
|
||||||
import { BinaryDataManager } from 'n8n-core';
|
import { BinaryDataService } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ExecutionPayload,
|
ExecutionPayload,
|
||||||
IExecutionBase,
|
IExecutionBase,
|
||||||
@@ -89,6 +89,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||||||
constructor(
|
constructor(
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
private readonly executionDataRepository: ExecutionDataRepository,
|
private readonly executionDataRepository: ExecutionDataRepository,
|
||||||
|
private readonly binaryDataService: BinaryDataService,
|
||||||
) {
|
) {
|
||||||
super(ExecutionEntity, dataSource.manager);
|
super(ExecutionEntity, dataSource.manager);
|
||||||
|
|
||||||
@@ -520,8 +521,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||||||
})
|
})
|
||||||
).map(({ id }) => id);
|
).map(({ id }) => id);
|
||||||
|
|
||||||
const binaryDataManager = BinaryDataManager.getInstance();
|
await this.binaryDataService.deleteManyByExecutionIds(executionIds);
|
||||||
await binaryDataManager.deleteBinaryDataByExecutionIds(executionIds);
|
|
||||||
|
|
||||||
this.logger.debug(`Hard-deleting ${executionIds.length} executions from database`, {
|
this.logger.debug(`Hard-deleting ${executionIds.length} executions from database`, {
|
||||||
executionIds,
|
executionIds,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LoggerProxy } from 'n8n-workflow';
|
|||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
|
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
|
||||||
import { BinaryDataManager } from 'n8n-core';
|
import { BinaryDataService } from 'n8n-core';
|
||||||
import { CacheService } from '@/services/cache.service';
|
import { CacheService } from '@/services/cache.service';
|
||||||
import { RedisServicePubSubPublisher } from '@/services/redis/RedisServicePubSubPublisher';
|
import { RedisServicePubSubPublisher } from '@/services/redis/RedisServicePubSubPublisher';
|
||||||
import { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber';
|
import { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber';
|
||||||
@@ -26,7 +26,7 @@ beforeAll(async () => {
|
|||||||
mockInstance(InternalHooks);
|
mockInstance(InternalHooks);
|
||||||
mockInstance(CacheService);
|
mockInstance(CacheService);
|
||||||
mockInstance(ExternalSecretsManager);
|
mockInstance(ExternalSecretsManager);
|
||||||
mockInstance(BinaryDataManager);
|
mockInstance(BinaryDataService);
|
||||||
mockInstance(MessageEventBus);
|
mockInstance(MessageEventBus);
|
||||||
mockInstance(LoadNodesAndCredentials);
|
mockInstance(LoadNodesAndCredentials);
|
||||||
mockInstance(CredentialTypes);
|
mockInstance(CredentialTypes);
|
||||||
@@ -41,7 +41,7 @@ test('worker initializes all its components', async () => {
|
|||||||
|
|
||||||
jest.spyOn(worker, 'init');
|
jest.spyOn(worker, 'init');
|
||||||
jest.spyOn(worker, 'initLicense').mockImplementation(async () => {});
|
jest.spyOn(worker, 'initLicense').mockImplementation(async () => {});
|
||||||
jest.spyOn(worker, 'initBinaryManager').mockImplementation(async () => {});
|
jest.spyOn(worker, 'initBinaryDataService').mockImplementation(async () => {});
|
||||||
jest.spyOn(worker, 'initExternalHooks').mockImplementation(async () => {});
|
jest.spyOn(worker, 'initExternalHooks').mockImplementation(async () => {});
|
||||||
jest.spyOn(worker, 'initExternalSecrets').mockImplementation(async () => {});
|
jest.spyOn(worker, 'initExternalSecrets').mockImplementation(async () => {});
|
||||||
jest.spyOn(worker, 'initEventBus').mockImplementation(async () => {});
|
jest.spyOn(worker, 'initEventBus').mockImplementation(async () => {});
|
||||||
@@ -64,7 +64,7 @@ test('worker initializes all its components', async () => {
|
|||||||
expect(worker.uniqueInstanceId).toContain('worker');
|
expect(worker.uniqueInstanceId).toContain('worker');
|
||||||
expect(worker.uniqueInstanceId.length).toBeGreaterThan(15);
|
expect(worker.uniqueInstanceId.length).toBeGreaterThan(15);
|
||||||
expect(worker.initLicense).toHaveBeenCalled();
|
expect(worker.initLicense).toHaveBeenCalled();
|
||||||
expect(worker.initBinaryManager).toHaveBeenCalled();
|
expect(worker.initBinaryDataService).toHaveBeenCalled();
|
||||||
expect(worker.initExternalHooks).toHaveBeenCalled();
|
expect(worker.initExternalHooks).toHaveBeenCalled();
|
||||||
expect(worker.initExternalSecrets).toHaveBeenCalled();
|
expect(worker.initExternalSecrets).toHaveBeenCalled();
|
||||||
expect(worker.initEventBus).toHaveBeenCalled();
|
expect(worker.initEventBus).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ beforeAll(async () => {
|
|||||||
user1 = await testDb.createUser({ globalRole: globalUserRole, apiKey: randomApiKey() });
|
user1 = await testDb.createUser({ globalRole: globalUserRole, apiKey: randomApiKey() });
|
||||||
user2 = await testDb.createUser({ globalRole: globalUserRole, apiKey: randomApiKey() });
|
user2 = await testDb.createUser({ globalRole: globalUserRole, apiKey: randomApiKey() });
|
||||||
|
|
||||||
// TODO: mock BinaryDataManager instead
|
// TODO: mock BinaryDataService instead
|
||||||
await utils.initBinaryManager();
|
await utils.initBinaryDataService();
|
||||||
await utils.initNodeTypes();
|
await utils.initNodeTypes();
|
||||||
|
|
||||||
workflowRunner = await utils.initActiveWorkflowRunner();
|
workflowRunner = await utils.initActiveWorkflowRunner();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { BinaryDataManager, UserSettings } from 'n8n-core';
|
import { BinaryDataService, UserSettings } from 'n8n-core';
|
||||||
import type { INode } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials';
|
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials';
|
||||||
import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials';
|
import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials';
|
||||||
@@ -72,11 +72,14 @@ export async function initNodeTypes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a BinaryManager for test runs.
|
* Initialize a BinaryDataService for test runs.
|
||||||
*/
|
*/
|
||||||
export async function initBinaryManager() {
|
export async function initBinaryDataService() {
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
const binaryDataService = new BinaryDataService();
|
||||||
await BinaryDataManager.init(binaryDataConfig);
|
|
||||||
|
await binaryDataService.init(config.getEnv('binaryDataManager'));
|
||||||
|
|
||||||
|
Container.set(BinaryDataService, binaryDataService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
"n8n-nodes-base": "workspace:*"
|
"n8n-nodes-base": "workspace:*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
|
||||||
"@n8n/client-oauth2": "workspace:*",
|
"@n8n/client-oauth2": "workspace:*",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"concat-stream": "^2.0.0",
|
"concat-stream": "^2.0.0",
|
||||||
"cron": "~1.7.2",
|
"cron": "~1.7.2",
|
||||||
"crypto-js": "~4.1.1",
|
"crypto-js": "~4.1.1",
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
|
"typedi": "^0.10.0",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
227
packages/core/src/BinaryData/BinaryData.service.ts
Normal file
227
packages/core/src/BinaryData/BinaryData.service.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { readFile, stat } from 'fs/promises';
|
||||||
|
import concatStream from 'concat-stream';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import { BINARY_ENCODING, LoggerProxy as Logger, IBinaryData } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { FileSystemManager } from './FileSystem.manager';
|
||||||
|
import { InvalidBinaryDataManagerError, InvalidBinaryDataModeError, areValidModes } from './utils';
|
||||||
|
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import type { BinaryData } from './types';
|
||||||
|
import type { INodeExecutionData } from 'n8n-workflow';
|
||||||
|
import { LogCatch } from '../decorators/LogCatch.decorator';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BinaryDataService {
|
||||||
|
private availableModes: BinaryData.Mode[] = [];
|
||||||
|
|
||||||
|
private mode: BinaryData.Mode = 'default';
|
||||||
|
|
||||||
|
private managers: Record<string, BinaryData.Manager> = {};
|
||||||
|
|
||||||
|
async init(config: BinaryData.Config) {
|
||||||
|
if (!areValidModes(config.availableModes)) throw new InvalidBinaryDataModeError();
|
||||||
|
|
||||||
|
this.availableModes = config.availableModes;
|
||||||
|
this.mode = config.mode;
|
||||||
|
|
||||||
|
if (this.availableModes.includes('filesystem')) {
|
||||||
|
this.managers.filesystem = new FileSystemManager(config.localStoragePath);
|
||||||
|
|
||||||
|
await this.managers.filesystem.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogCatch((error) => Logger.error('Failed to copy binary data file', { error }))
|
||||||
|
async copyBinaryFile(binaryData: IBinaryData, path: string, executionId: string) {
|
||||||
|
const manager = this.managers[this.mode];
|
||||||
|
|
||||||
|
if (!manager) {
|
||||||
|
const { size } = await stat(path);
|
||||||
|
binaryData.fileSize = prettyBytes(size);
|
||||||
|
binaryData.data = await readFile(path, { encoding: BINARY_ENCODING });
|
||||||
|
|
||||||
|
return binaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = await manager.copyByPath(path, executionId);
|
||||||
|
binaryData.id = this.createIdentifier(identifier);
|
||||||
|
binaryData.data = this.mode; // clear binary data from memory
|
||||||
|
|
||||||
|
const fileSize = await manager.getSize(identifier);
|
||||||
|
binaryData.fileSize = prettyBytes(fileSize);
|
||||||
|
|
||||||
|
await manager.storeMetadata(identifier, {
|
||||||
|
fileName: binaryData.fileName,
|
||||||
|
mimeType: binaryData.mimeType,
|
||||||
|
fileSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return binaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogCatch((error) => Logger.error('Failed to write binary data file', { error }))
|
||||||
|
async store(binaryData: IBinaryData, input: Buffer | Readable, executionId: string) {
|
||||||
|
const manager = this.managers[this.mode];
|
||||||
|
|
||||||
|
if (!manager) {
|
||||||
|
const buffer = await this.binaryToBuffer(input);
|
||||||
|
binaryData.data = buffer.toString(BINARY_ENCODING);
|
||||||
|
binaryData.fileSize = prettyBytes(buffer.length);
|
||||||
|
|
||||||
|
return binaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = await manager.store(input, executionId);
|
||||||
|
binaryData.id = this.createIdentifier(identifier);
|
||||||
|
binaryData.data = this.mode; // clear binary data from memory
|
||||||
|
|
||||||
|
const fileSize = await manager.getSize(identifier);
|
||||||
|
binaryData.fileSize = prettyBytes(fileSize);
|
||||||
|
|
||||||
|
await manager.storeMetadata(identifier, {
|
||||||
|
fileName: binaryData.fileName,
|
||||||
|
mimeType: binaryData.mimeType,
|
||||||
|
fileSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return binaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async binaryToBuffer(body: Buffer | Readable) {
|
||||||
|
return new Promise<Buffer>((resolve) => {
|
||||||
|
if (Buffer.isBuffer(body)) resolve(body);
|
||||||
|
else body.pipe(concatStream(resolve));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAsStream(identifier: string, chunkSize?: number) {
|
||||||
|
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||||
|
|
||||||
|
return this.getManager(mode).getStream(id, chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBinaryDataBuffer(binaryData: IBinaryData) {
|
||||||
|
if (binaryData.id) return this.retrieveBinaryDataByIdentifier(binaryData.id);
|
||||||
|
|
||||||
|
return Buffer.from(binaryData.data, BINARY_ENCODING);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveBinaryDataByIdentifier(identifier: string) {
|
||||||
|
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||||
|
|
||||||
|
return this.getManager(mode).getBuffer(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPath(identifier: string) {
|
||||||
|
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||||
|
|
||||||
|
return this.getManager(mode).getPath(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(identifier: string) {
|
||||||
|
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||||
|
|
||||||
|
return this.getManager(mode).getMetadata(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteManyByExecutionIds(executionIds: string[]) {
|
||||||
|
const manager = this.getManager(this.mode);
|
||||||
|
|
||||||
|
if (!manager) return;
|
||||||
|
|
||||||
|
await manager.deleteManyByExecutionIds(executionIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogCatch((error) =>
|
||||||
|
Logger.error('Failed to copy all binary data files for execution', { error }),
|
||||||
|
)
|
||||||
|
async duplicateBinaryData(inputData: Array<INodeExecutionData[] | null>, executionId: string) {
|
||||||
|
if (inputData && this.managers[this.mode]) {
|
||||||
|
const returnInputData = (inputData as INodeExecutionData[][]).map(
|
||||||
|
async (executionDataArray) => {
|
||||||
|
if (executionDataArray) {
|
||||||
|
return Promise.all(
|
||||||
|
executionDataArray.map(async (executionData) => {
|
||||||
|
if (executionData.binary) {
|
||||||
|
return this.duplicateBinaryDataInExecData(executionData, executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executionData;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executionDataArray;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(returnInputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputData as INodeExecutionData[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// private methods
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
private createIdentifier(filename: string) {
|
||||||
|
return `${this.mode}:${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private splitBinaryModeFileId(fileId: string) {
|
||||||
|
const [mode, id] = fileId.split(':');
|
||||||
|
|
||||||
|
return { mode, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async duplicateBinaryDataInExecData(
|
||||||
|
executionData: INodeExecutionData,
|
||||||
|
executionId: string,
|
||||||
|
) {
|
||||||
|
const manager = this.managers[this.mode];
|
||||||
|
|
||||||
|
if (executionData.binary) {
|
||||||
|
const binaryDataKeys = Object.keys(executionData.binary);
|
||||||
|
const bdPromises = binaryDataKeys.map(async (key: string) => {
|
||||||
|
if (!executionData.binary) {
|
||||||
|
return { key, newId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryDataId = executionData.binary[key].id;
|
||||||
|
if (!binaryDataId) {
|
||||||
|
return { key, newId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager
|
||||||
|
?.copyByIdentifier(this.splitBinaryModeFileId(binaryDataId).id, executionId)
|
||||||
|
.then((filename) => ({
|
||||||
|
newId: this.createIdentifier(filename),
|
||||||
|
key,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(bdPromises).then((b) => {
|
||||||
|
return b.reduce((acc, curr) => {
|
||||||
|
if (acc.binary && curr) {
|
||||||
|
acc.binary[curr.key].id = curr.newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, executionData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return executionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getManager(mode: string) {
|
||||||
|
const manager = this.managers[mode];
|
||||||
|
|
||||||
|
if (manager) return manager;
|
||||||
|
|
||||||
|
throw new InvalidBinaryDataManagerError(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
packages/core/src/BinaryData/FileSystem.manager.ts
Normal file
142
packages/core/src/BinaryData/FileSystem.manager.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { createReadStream } from 'fs';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { FileNotFoundError } from '../errors';
|
||||||
|
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import type { BinaryMetadata } from 'n8n-workflow';
|
||||||
|
import type { BinaryData } from './types';
|
||||||
|
|
||||||
|
const EXECUTION_ID_EXTRACTOR =
|
||||||
|
/^(\w+)(?:[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})$/;
|
||||||
|
|
||||||
|
export class FileSystemManager implements BinaryData.Manager {
|
||||||
|
constructor(private storagePath: string) {}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.ensureDirExists(this.storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPath(identifier: string) {
|
||||||
|
return this.resolvePath(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSize(identifier: string) {
|
||||||
|
const filePath = this.getPath(identifier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
return stats.size;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to find binary data file in filesystem', { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStream(identifier: string, chunkSize?: number) {
|
||||||
|
const filePath = this.getPath(identifier);
|
||||||
|
|
||||||
|
return createReadStream(filePath, { highWaterMark: chunkSize });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBuffer(identifier: string) {
|
||||||
|
const filePath = this.getPath(identifier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fs.readFile(filePath);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Error finding file: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeMetadata(identifier: string, metadata: BinaryMetadata) {
|
||||||
|
const filePath = this.resolvePath(`${identifier}.metadata`);
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(metadata), { encoding: 'utf-8' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(identifier: string): Promise<BinaryMetadata> {
|
||||||
|
const filePath = this.resolvePath(`${identifier}.metadata`);
|
||||||
|
|
||||||
|
return jsonParse(await fs.readFile(filePath, { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async store(binaryData: Buffer | Readable, executionId: string) {
|
||||||
|
const identifier = this.createIdentifier(executionId);
|
||||||
|
const filePath = this.getPath(identifier);
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, binaryData);
|
||||||
|
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOne(identifier: string) {
|
||||||
|
const filePath = this.getPath(identifier);
|
||||||
|
|
||||||
|
return fs.rm(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteManyByExecutionIds(executionIds: string[]) {
|
||||||
|
const set = new Set(executionIds);
|
||||||
|
const fileNames = await fs.readdir(this.storagePath);
|
||||||
|
const deletedIds = [];
|
||||||
|
|
||||||
|
for (const fileName of fileNames) {
|
||||||
|
const executionId = fileName.match(EXECUTION_ID_EXTRACTOR)?.[1];
|
||||||
|
|
||||||
|
if (executionId && set.has(executionId)) {
|
||||||
|
const filePath = this.resolvePath(fileName);
|
||||||
|
|
||||||
|
await Promise.all([fs.rm(filePath), fs.rm(`${filePath}.metadata`)]);
|
||||||
|
|
||||||
|
deletedIds.push(executionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyByPath(filePath: string, executionId: string) {
|
||||||
|
const identifier = this.createIdentifier(executionId);
|
||||||
|
|
||||||
|
await fs.cp(filePath, this.getPath(identifier));
|
||||||
|
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyByIdentifier(identifier: string, executionId: string) {
|
||||||
|
const newIdentifier = this.createIdentifier(executionId);
|
||||||
|
|
||||||
|
await fs.copyFile(this.resolvePath(identifier), this.resolvePath(newIdentifier));
|
||||||
|
|
||||||
|
return newIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// private methods
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
private async ensureDirExists(dir: string) {
|
||||||
|
try {
|
||||||
|
await fs.access(dir);
|
||||||
|
} catch {
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createIdentifier(executionId: string) {
|
||||||
|
return [executionId, uuid()].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePath(...args: string[]) {
|
||||||
|
const returnPath = path.join(this.storagePath, ...args);
|
||||||
|
|
||||||
|
if (path.relative(this.storagePath, returnPath).startsWith('..')) {
|
||||||
|
throw new FileNotFoundError('Invalid path detected');
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
packages/core/src/BinaryData/types.ts
Normal file
44
packages/core/src/BinaryData/types.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Readable } from 'stream';
|
||||||
|
import type { BinaryMetadata } from 'n8n-workflow';
|
||||||
|
import type { BINARY_DATA_MODES } from './utils';
|
||||||
|
|
||||||
|
export namespace BinaryData {
|
||||||
|
export type Mode = (typeof BINARY_DATA_MODES)[number];
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
mode: 'default' | 'filesystem';
|
||||||
|
availableModes: string[];
|
||||||
|
localStoragePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Manager {
|
||||||
|
init(): Promise<void>;
|
||||||
|
|
||||||
|
store(binaryData: Buffer | Readable, executionId: string): Promise<string>;
|
||||||
|
getPath(identifier: string): string;
|
||||||
|
|
||||||
|
// @TODO: Refactor to use identifier
|
||||||
|
getSize(path: string): Promise<number>;
|
||||||
|
|
||||||
|
getBuffer(identifier: string): Promise<Buffer>;
|
||||||
|
getStream(identifier: string, chunkSize?: number): Readable;
|
||||||
|
|
||||||
|
// @TODO: Refactor out - not needed for object storage
|
||||||
|
storeMetadata(identifier: string, metadata: BinaryMetadata): Promise<void>;
|
||||||
|
|
||||||
|
// @TODO: Refactor out - not needed for object storage
|
||||||
|
getMetadata(identifier: string): Promise<BinaryMetadata>;
|
||||||
|
|
||||||
|
// @TODO: Refactor to also use `workflowId` to support full path-like identifier:
|
||||||
|
// `workflows/{workflowId}/executions/{executionId}/binary_data/{fileId}`
|
||||||
|
copyByPath(path: string, executionId: string): Promise<string>;
|
||||||
|
|
||||||
|
copyByIdentifier(identifier: string, prefix: string): Promise<string>;
|
||||||
|
|
||||||
|
deleteOne(identifier: string): Promise<void>;
|
||||||
|
|
||||||
|
// @TODO: Refactor to also receive `workflowId` to support full path-like identifier:
|
||||||
|
// `workflows/{workflowId}/executions/{executionId}/binary_data/{fileId}`
|
||||||
|
deleteManyByExecutionIds(executionIds: string[]): Promise<string[]>;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/core/src/BinaryData/utils.ts
Normal file
25
packages/core/src/BinaryData/utils.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { BinaryData } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modes for storing binary data:
|
||||||
|
* - `default` (in memory)
|
||||||
|
* - `filesystem` (on disk)
|
||||||
|
* - `s3` (S3-compatible storage)
|
||||||
|
*/
|
||||||
|
export const BINARY_DATA_MODES = ['default', 'filesystem', 's3'] as const;
|
||||||
|
|
||||||
|
export function areValidModes(modes: string[]): modes is BinaryData.Mode[] {
|
||||||
|
return modes.every((m) => BINARY_DATA_MODES.includes(m as BinaryData.Mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidBinaryDataModeError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super(`Invalid binary data mode. Valid modes: ${BINARY_DATA_MODES.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidBinaryDataManagerError extends Error {
|
||||||
|
constructor(mode: string) {
|
||||||
|
super('No binary data manager found for mode: ' + mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { createReadStream } from 'fs';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import type { Readable } from 'stream';
|
|
||||||
import type { BinaryMetadata } from 'n8n-workflow';
|
|
||||||
import { jsonParse } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { IBinaryDataConfig } from '../Interfaces';
|
|
||||||
import type { IBinaryDataManager } from '../Interfaces';
|
|
||||||
import { FileNotFoundError } from '../errors';
|
|
||||||
|
|
||||||
const executionExtractionRegexp =
|
|
||||||
/^(\w+)(?:[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})$/;
|
|
||||||
|
|
||||||
export class BinaryDataFileSystem implements IBinaryDataManager {
|
|
||||||
private storagePath: string;
|
|
||||||
|
|
||||||
constructor(config: IBinaryDataConfig) {
|
|
||||||
this.storagePath = config.localStoragePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.assertFolder(this.storagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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(binaryData: Buffer | Readable, executionId: string): Promise<string> {
|
|
||||||
const binaryDataId = this.generateFileName(executionId);
|
|
||||||
await this.saveToLocalStorage(binaryData, binaryDataId);
|
|
||||||
return binaryDataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBinaryStream(identifier: string, chunkSize?: number): Readable {
|
|
||||||
return createReadStream(this.getBinaryPath(identifier), { highWaterMark: chunkSize });
|
|
||||||
}
|
|
||||||
|
|
||||||
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
|
|
||||||
return this.retrieveFromLocalStorage(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBinaryPath(identifier: string): string {
|
|
||||||
return this.resolveStoragePath(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetadataPath(identifier: string): string {
|
|
||||||
return this.resolveStoragePath(`${identifier}.metadata`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string> {
|
|
||||||
const newBinaryDataId = this.generateFileName(prefix);
|
|
||||||
|
|
||||||
await fs.copyFile(
|
|
||||||
this.resolveStoragePath(binaryDataId),
|
|
||||||
this.resolveStoragePath(newBinaryDataId),
|
|
||||||
);
|
|
||||||
return newBinaryDataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteBinaryDataByExecutionIds(executionIds: string[]): Promise<string[]> {
|
|
||||||
const set = new Set(executionIds);
|
|
||||||
const fileNames = await fs.readdir(this.storagePath);
|
|
||||||
const deletedIds = [];
|
|
||||||
for (const fileName of fileNames) {
|
|
||||||
const executionId = fileName.match(executionExtractionRegexp)?.[1];
|
|
||||||
if (executionId && set.has(executionId)) {
|
|
||||||
const filePath = this.resolveStoragePath(fileName);
|
|
||||||
await Promise.all([fs.rm(filePath), fs.rm(`${filePath}.metadata`)]);
|
|
||||||
deletedIds.push(executionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deletedIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteBinaryDataByIdentifier(identifier: string): Promise<void> {
|
|
||||||
return this.deleteFromLocalStorage(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async assertFolder(folder: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await fs.access(folder);
|
|
||||||
} catch {
|
|
||||||
await fs.mkdir(folder, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateFileName(prefix: string): string {
|
|
||||||
return [prefix, uuid()].join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deleteFromLocalStorage(identifier: string) {
|
|
||||||
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(binaryData: Buffer | Readable, identifier: string) {
|
|
||||||
await fs.writeFile(this.getBinaryPath(identifier), binaryData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async retrieveFromLocalStorage(identifier: string): Promise<Buffer> {
|
|
||||||
const filePath = this.getBinaryPath(identifier);
|
|
||||||
try {
|
|
||||||
return await fs.readFile(filePath);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Error finding file: ${filePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveStoragePath(...args: string[]) {
|
|
||||||
const returnPath = path.join(this.storagePath, ...args);
|
|
||||||
if (path.relative(this.storagePath, returnPath).startsWith('..'))
|
|
||||||
throw new FileNotFoundError('Invalid path detected');
|
|
||||||
return returnPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
import { readFile, stat } from 'fs/promises';
|
|
||||||
import type { BinaryMetadata, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
import prettyBytes from 'pretty-bytes';
|
|
||||||
import type { Readable } from 'stream';
|
|
||||||
import { BINARY_ENCODING, LoggerProxy as Logger, IBinaryData } from 'n8n-workflow';
|
|
||||||
import { IBinaryDataConfig } from '../Interfaces';
|
|
||||||
import type { IBinaryDataManager } from '../Interfaces';
|
|
||||||
import { BinaryDataFileSystem } from './FileSystem';
|
|
||||||
import { binaryToBuffer } from './utils';
|
|
||||||
import { LogCatch } from '../decorators/LogCatch.decorator';
|
|
||||||
|
|
||||||
export class BinaryDataManager {
|
|
||||||
static instance: BinaryDataManager | undefined;
|
|
||||||
|
|
||||||
private managers: {
|
|
||||||
[key: string]: IBinaryDataManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
private binaryDataMode: string;
|
|
||||||
|
|
||||||
private availableModes: string[];
|
|
||||||
|
|
||||||
constructor(config: IBinaryDataConfig) {
|
|
||||||
this.binaryDataMode = config.mode;
|
|
||||||
this.availableModes = config.availableModes.split(',');
|
|
||||||
this.managers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static async init(config: IBinaryDataConfig, mainManager = false): Promise<void> {
|
|
||||||
if (BinaryDataManager.instance) {
|
|
||||||
throw new Error('Binary Data Manager already initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryDataManager.instance = new BinaryDataManager(config);
|
|
||||||
|
|
||||||
if (BinaryDataManager.instance.availableModes.includes('filesystem')) {
|
|
||||||
BinaryDataManager.instance.managers.filesystem = new BinaryDataFileSystem(config);
|
|
||||||
await BinaryDataManager.instance.managers.filesystem.init(mainManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInstance(): BinaryDataManager {
|
|
||||||
if (!BinaryDataManager.instance) {
|
|
||||||
throw new Error('Binary Data Manager not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
return BinaryDataManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
@LogCatch((error) => Logger.error('Failed to copy binary data file', { error }))
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@LogCatch((error) => Logger.error('Failed to write binary data file', { error }))
|
|
||||||
async storeBinaryData(
|
|
||||||
binaryData: IBinaryData,
|
|
||||||
input: Buffer | Readable,
|
|
||||||
executionId: string,
|
|
||||||
): Promise<IBinaryData> {
|
|
||||||
// If a manager handles this binary, return the binary data with its reference id.
|
|
||||||
const manager = this.managers[this.binaryDataMode];
|
|
||||||
if (manager) {
|
|
||||||
const identifier = await manager.storeBinaryData(input, 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 buffer = await binaryToBuffer(input);
|
|
||||||
binaryData.data = buffer.toString(BINARY_ENCODING);
|
|
||||||
binaryData.fileSize = prettyBytes(buffer.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 getBinaryDataBuffer(binaryData: IBinaryData): Promise<Buffer> {
|
|
||||||
if (binaryData.id) {
|
|
||||||
return this.retrieveBinaryDataByIdentifier(binaryData.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Buffer.from(binaryData.data, BINARY_ENCODING);
|
|
||||||
}
|
|
||||||
|
|
||||||
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
|
|
||||||
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
|
||||||
if (this.managers[mode]) {
|
|
||||||
return this.managers[mode].retrieveBinaryDataByIdentifier(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 deleteBinaryDataByExecutionIds(executionIds: string[]): Promise<void> {
|
|
||||||
if (this.managers[this.binaryDataMode]) {
|
|
||||||
await this.managers[this.binaryDataMode].deleteBinaryDataByExecutionIds(executionIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@LogCatch((error) =>
|
|
||||||
Logger.error('Failed to copy all binary data files for execution', { error }),
|
|
||||||
)
|
|
||||||
async duplicateBinaryData(
|
|
||||||
inputData: Array<INodeExecutionData[] | null>,
|
|
||||||
executionId: string,
|
|
||||||
): Promise<INodeExecutionData[][]> {
|
|
||||||
if (inputData && this.managers[this.binaryDataMode]) {
|
|
||||||
const returnInputData = (inputData as INodeExecutionData[][]).map(
|
|
||||||
async (executionDataArray) => {
|
|
||||||
if (executionDataArray) {
|
|
||||||
return Promise.all(
|
|
||||||
executionDataArray.map(async (executionData) => {
|
|
||||||
if (executionData.binary) {
|
|
||||||
return this.duplicateBinaryDataInExecData(executionData, executionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return executionData;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return executionDataArray;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.all(returnInputData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputData as INodeExecutionData[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateBinaryId(filename: string) {
|
|
||||||
return `${this.binaryDataMode}:${filename}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private splitBinaryModeFileId(fileId: string): { mode: string; id: string } {
|
|
||||||
const [mode, id] = fileId.split(':');
|
|
||||||
return { mode, id };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async duplicateBinaryDataInExecData(
|
|
||||||
executionData: INodeExecutionData,
|
|
||||||
executionId: string,
|
|
||||||
): Promise<INodeExecutionData> {
|
|
||||||
const binaryManager = this.managers[this.binaryDataMode];
|
|
||||||
|
|
||||||
if (executionData.binary) {
|
|
||||||
const binaryDataKeys = Object.keys(executionData.binary);
|
|
||||||
const bdPromises = binaryDataKeys.map(async (key: string) => {
|
|
||||||
if (!executionData.binary) {
|
|
||||||
return { key, newId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const binaryDataId = executionData.binary[key].id;
|
|
||||||
if (!binaryDataId) {
|
|
||||||
return { key, newId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
return binaryManager
|
|
||||||
?.duplicateBinaryDataByIdentifier(
|
|
||||||
this.splitBinaryModeFileId(binaryDataId).id,
|
|
||||||
executionId,
|
|
||||||
)
|
|
||||||
.then((filename) => ({
|
|
||||||
newId: this.generateBinaryId(filename),
|
|
||||||
key,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(bdPromises).then((b) => {
|
|
||||||
return b.reduce((acc, curr) => {
|
|
||||||
if (acc.binary && curr) {
|
|
||||||
acc.binary[curr.key].id = curr.newId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, executionData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return executionData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import concatStream from 'concat-stream';
|
|
||||||
import type { Readable } from 'stream';
|
|
||||||
|
|
||||||
export const binaryToBuffer = async (body: Buffer | Readable) =>
|
|
||||||
new Promise<Buffer>((resolve) => {
|
|
||||||
if (Buffer.isBuffer(body)) resolve(body);
|
|
||||||
else body.pipe(concatStream(resolve));
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { Readable } from 'stream';
|
|
||||||
import type {
|
import type {
|
||||||
IPollResponse,
|
IPollResponse,
|
||||||
ITriggerResponse,
|
ITriggerResponse,
|
||||||
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||||
BinaryMetadata,
|
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -34,27 +32,6 @@ export interface IWorkflowData {
|
|||||||
triggerResponses?: ITriggerResponse[];
|
triggerResponses?: ITriggerResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBinaryDataConfig {
|
|
||||||
mode: 'default' | 'filesystem';
|
|
||||||
availableModes: string;
|
|
||||||
localStoragePath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBinaryDataManager {
|
|
||||||
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(binaryData: Buffer | Readable, executionId: string): Promise<string>;
|
|
||||||
retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>;
|
|
||||||
getBinaryPath(identifier: string): string;
|
|
||||||
getBinaryStream(identifier: string, chunkSize?: number): Readable;
|
|
||||||
deleteBinaryDataByIdentifier(identifier: string): Promise<void>;
|
|
||||||
duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string>;
|
|
||||||
deleteBinaryDataByExecutionIds(executionIds: string[]): Promise<string[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace n8n {
|
export namespace n8n {
|
||||||
export interface PackageJson {
|
export interface PackageJson {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -117,8 +117,7 @@ import type { RequestPromiseOptions } from 'request-promise-native';
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import url, { URL, URLSearchParams } from 'url';
|
import url, { URL, URLSearchParams } from 'url';
|
||||||
|
|
||||||
import { BinaryDataManager } from './BinaryDataManager';
|
import { BinaryDataService } from './BinaryData/BinaryData.service';
|
||||||
import { binaryToBuffer } from './BinaryDataManager/utils';
|
|
||||||
import {
|
import {
|
||||||
BINARY_DATA_STORAGE_PATH,
|
BINARY_DATA_STORAGE_PATH,
|
||||||
BLOCK_FILE_ACCESS_TO_N8N_FILES,
|
BLOCK_FILE_ACCESS_TO_N8N_FILES,
|
||||||
@@ -132,14 +131,15 @@ import {
|
|||||||
import { extractValue } from './ExtractValue';
|
import { extractValue } from './ExtractValue';
|
||||||
import type { ExtendedValidationResult, IResponseError, IWorkflowSettings } from './Interfaces';
|
import type { ExtendedValidationResult, IResponseError, IWorkflowSettings } from './Interfaces';
|
||||||
import { getClientCredentialsToken } from './OAuth2Helper';
|
import { getClientCredentialsToken } from './OAuth2Helper';
|
||||||
import { getSecretsProxy } from './Secrets';
|
|
||||||
import { getUserN8nFolderPath } from './UserSettings';
|
|
||||||
import {
|
import {
|
||||||
getAllWorkflowExecutionMetadata,
|
getAllWorkflowExecutionMetadata,
|
||||||
getWorkflowExecutionMetadata,
|
getWorkflowExecutionMetadata,
|
||||||
setAllWorkflowExecutionMetadata,
|
setAllWorkflowExecutionMetadata,
|
||||||
setWorkflowExecutionMetadata,
|
setWorkflowExecutionMetadata,
|
||||||
} from './WorkflowExecutionMetadata';
|
} from './WorkflowExecutionMetadata';
|
||||||
|
import { getSecretsProxy } from './Secrets';
|
||||||
|
import { getUserN8nFolderPath } from './UserSettings';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
axios.defaults.timeout = 300000;
|
axios.defaults.timeout = 300000;
|
||||||
// Prevent axios from adding x-form-www-urlencoded headers by default
|
// Prevent axios from adding x-form-www-urlencoded headers by default
|
||||||
@@ -774,9 +774,9 @@ export async function proxyRequestToAxios(
|
|||||||
let responseData = response.data;
|
let responseData = response.data;
|
||||||
|
|
||||||
if (Buffer.isBuffer(responseData) || responseData instanceof Readable) {
|
if (Buffer.isBuffer(responseData) || responseData instanceof Readable) {
|
||||||
responseData = await binaryToBuffer(responseData).then((buffer) =>
|
responseData = await Container.get(BinaryDataService)
|
||||||
buffer.toString('utf-8'),
|
.binaryToBuffer(responseData)
|
||||||
);
|
.then((buffer) => buffer.toString('utf-8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configObject.simple === false) {
|
if (configObject.simple === false) {
|
||||||
@@ -941,21 +941,21 @@ async function httpRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getBinaryPath(binaryDataId: string): string {
|
export function getBinaryPath(binaryDataId: string): string {
|
||||||
return BinaryDataManager.getInstance().getBinaryPath(binaryDataId);
|
return Container.get(BinaryDataService).getPath(binaryDataId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns binary file metadata
|
* Returns binary file metadata
|
||||||
*/
|
*/
|
||||||
export async function getBinaryMetadata(binaryDataId: string): Promise<BinaryMetadata> {
|
export async function getBinaryMetadata(binaryDataId: string): Promise<BinaryMetadata> {
|
||||||
return BinaryDataManager.getInstance().getBinaryMetadata(binaryDataId);
|
return Container.get(BinaryDataService).getMetadata(binaryDataId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns binary file stream for piping
|
* Returns binary file stream for piping
|
||||||
*/
|
*/
|
||||||
export function getBinaryStream(binaryDataId: string, chunkSize?: number): Readable {
|
export function getBinaryStream(binaryDataId: string, chunkSize?: number): Readable {
|
||||||
return BinaryDataManager.getInstance().getBinaryStream(binaryDataId, chunkSize);
|
return Container.get(BinaryDataService).getAsStream(binaryDataId, chunkSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertBinaryData(
|
export function assertBinaryData(
|
||||||
@@ -992,7 +992,7 @@ export async function getBinaryDataBuffer(
|
|||||||
inputIndex: number,
|
inputIndex: number,
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const binaryData = inputData.main[inputIndex]![itemIndex]!.binary![propertyName]!;
|
const binaryData = inputData.main[inputIndex]![itemIndex]!.binary![propertyName]!;
|
||||||
return BinaryDataManager.getInstance().getBinaryDataBuffer(binaryData);
|
return Container.get(BinaryDataService).getBinaryDataBuffer(binaryData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1008,7 +1008,7 @@ export async function setBinaryDataBuffer(
|
|||||||
binaryData: Buffer | Readable,
|
binaryData: Buffer | Readable,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
): Promise<IBinaryData> {
|
): Promise<IBinaryData> {
|
||||||
return BinaryDataManager.getInstance().storeBinaryData(data, binaryData, executionId);
|
return Container.get(BinaryDataService).store(data, binaryData, executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyBinaryFile(
|
export async function copyBinaryFile(
|
||||||
@@ -1061,7 +1061,7 @@ export async function copyBinaryFile(
|
|||||||
returnData.fileName = path.parse(filePath).base;
|
returnData.fileName = path.parse(filePath).base;
|
||||||
}
|
}
|
||||||
|
|
||||||
return BinaryDataManager.getInstance().copyBinaryFile(returnData, filePath, executionId);
|
return Container.get(BinaryDataService).copyBinaryFile(returnData, filePath, executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2573,7 +2573,8 @@ const getBinaryHelperFunctions = ({
|
|||||||
getBinaryPath,
|
getBinaryPath,
|
||||||
getBinaryStream,
|
getBinaryStream,
|
||||||
getBinaryMetadata,
|
getBinaryMetadata,
|
||||||
binaryToBuffer,
|
binaryToBuffer: async (body: Buffer | Readable) =>
|
||||||
|
Container.get(BinaryDataService).binaryToBuffer(body),
|
||||||
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) =>
|
||||||
@@ -2761,7 +2762,7 @@ export function getExecuteFunctions(
|
|||||||
parentWorkflowSettings: workflow.settings,
|
parentWorkflowSettings: workflow.settings,
|
||||||
})
|
})
|
||||||
.then(async (result) =>
|
.then(async (result) =>
|
||||||
BinaryDataManager.getInstance().duplicateBinaryData(
|
Container.get(BinaryDataService).duplicateBinaryData(
|
||||||
result,
|
result,
|
||||||
additionalData.executionId!,
|
additionalData.executionId!,
|
||||||
),
|
),
|
||||||
@@ -2833,7 +2834,8 @@ export function getExecuteFunctions(
|
|||||||
);
|
);
|
||||||
return dataProxy.getDataProxy();
|
return dataProxy.getDataProxy();
|
||||||
},
|
},
|
||||||
binaryToBuffer,
|
binaryToBuffer: async (body: Buffer | Readable) =>
|
||||||
|
Container.get(BinaryDataService).binaryToBuffer(body),
|
||||||
async putExecutionToWait(waitTill: Date): Promise<void> {
|
async putExecutionToWait(waitTill: Date): Promise<void> {
|
||||||
runExecutionData.waitTill = waitTill;
|
runExecutionData.waitTill = waitTill;
|
||||||
if (additionalData.setExecutionStatus) {
|
if (additionalData.setExecutionStatus) {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
|||||||
import * as UserSettings from './UserSettings';
|
import * as UserSettings from './UserSettings';
|
||||||
|
|
||||||
export * from './ActiveWorkflows';
|
export * from './ActiveWorkflows';
|
||||||
export * from './BinaryDataManager';
|
export * from './BinaryData/BinaryData.service';
|
||||||
|
export * from './BinaryData/types';
|
||||||
export * from './ClassLoader';
|
export * from './ClassLoader';
|
||||||
export * from './Constants';
|
export * from './Constants';
|
||||||
export * from './Credentials';
|
export * from './Credentials';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BinaryDataManager } from '@/BinaryDataManager';
|
|
||||||
import {
|
import {
|
||||||
getBinaryDataBuffer,
|
getBinaryDataBuffer,
|
||||||
parseIncomingMessage,
|
parseIncomingMessage,
|
||||||
@@ -16,25 +15,24 @@ import type {
|
|||||||
Workflow,
|
Workflow,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { initLogger } from './helpers/utils';
|
import { initLogger } from './helpers/utils';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
const temporaryDir = mkdtempSync(join(tmpdir(), 'n8n'));
|
const temporaryDir = mkdtempSync(join(tmpdir(), 'n8n'));
|
||||||
|
|
||||||
describe('NodeExecuteFunctions', () => {
|
describe('NodeExecuteFunctions', () => {
|
||||||
describe('test binary data helper methods', () => {
|
describe('test binary data helper methods', () => {
|
||||||
// Reset BinaryDataManager for each run. This is a dirty operation, as individual managers are not cleaned.
|
|
||||||
beforeEach(() => {
|
|
||||||
BinaryDataManager.instance = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode", async () => {
|
test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode", async () => {
|
||||||
// Setup a 'default' binary data manager instance
|
// Setup a 'default' binary data manager instance
|
||||||
await BinaryDataManager.init({
|
Container.set(BinaryDataService, new BinaryDataService());
|
||||||
|
|
||||||
|
await Container.get(BinaryDataService).init({
|
||||||
mode: 'default',
|
mode: 'default',
|
||||||
availableModes: 'default',
|
availableModes: ['default'],
|
||||||
localStoragePath: temporaryDir,
|
localStoragePath: temporaryDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,10 +78,12 @@ describe('NodeExecuteFunctions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode", async () => {
|
test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode", async () => {
|
||||||
|
Container.set(BinaryDataService, new BinaryDataService());
|
||||||
|
|
||||||
// Setup a 'filesystem' binary data manager instance
|
// Setup a 'filesystem' binary data manager instance
|
||||||
await BinaryDataManager.init({
|
await Container.get(BinaryDataService).init({
|
||||||
mode: 'filesystem',
|
mode: 'filesystem',
|
||||||
availableModes: 'filesystem',
|
availableModes: ['filesystem'],
|
||||||
localStoragePath: temporaryDir,
|
localStoragePath: temporaryDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
import { getWorkflowFilenames, initBinaryDataManager, testWorkflows } from '@test/nodes/Helpers';
|
import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
|
||||||
|
|
||||||
const workflows = getWorkflowFilenames(__dirname);
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ describe('Test S3 V1 Node', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
||||||
|
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
|
|
||||||
nock.disableNetConnect();
|
nock.disableNetConnect();
|
||||||
mock = nock('https://bucket.s3.eu-central-1.amazonaws.com');
|
mock = nock('https://bucket.s3.eu-central-1.amazonaws.com');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
import { getWorkflowFilenames, initBinaryDataManager, testWorkflows } from '@test/nodes/Helpers';
|
import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
|
||||||
|
|
||||||
const workflows = getWorkflowFilenames(__dirname);
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ describe('Test S3 V2 Node', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
||||||
|
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
|
|
||||||
nock.disableNetConnect();
|
nock.disableNetConnect();
|
||||||
mock = nock('https://bucket.s3.eu-central-1.amazonaws.com');
|
mock = nock('https://bucket.s3.eu-central-1.amazonaws.com');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { NodeVM } from '@n8n/vm2';
|
|||||||
import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow';
|
import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow';
|
||||||
import { NodeHelpers } from 'n8n-workflow';
|
import { NodeHelpers } from 'n8n-workflow';
|
||||||
import { normalizeItems } from 'n8n-core';
|
import { normalizeItems } from 'n8n-core';
|
||||||
import { testWorkflows, getWorkflowFilenames, initBinaryDataManager } from '@test/nodes/Helpers';
|
import { testWorkflows, getWorkflowFilenames, initBinaryDataService } from '@test/nodes/Helpers';
|
||||||
import { Code } from '../Code.node';
|
import { Code } from '../Code.node';
|
||||||
import { ValidationError } from '../ValidationError';
|
import { ValidationError } from '../ValidationError';
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ describe('Test Code Node', () => {
|
|||||||
const workflows = getWorkflowFilenames(__dirname);
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWorkflows(workflows);
|
testWorkflows(workflows);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { IDataObject } from 'n8n-workflow';
|
|||||||
import {
|
import {
|
||||||
getResultNodeData,
|
getResultNodeData,
|
||||||
setup,
|
setup,
|
||||||
initBinaryDataManager,
|
initBinaryDataService,
|
||||||
readJsonFileSync,
|
readJsonFileSync,
|
||||||
} from '@test/nodes/Helpers';
|
} from '@test/nodes/Helpers';
|
||||||
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
|
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
|
||||||
@@ -16,7 +16,7 @@ import os from 'node:os';
|
|||||||
if (os.platform() !== 'win32') {
|
if (os.platform() !== 'win32') {
|
||||||
describe('Execute Compression Node', () => {
|
describe('Execute Compression Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflowData = readJsonFileSync('nodes/Compression/test/node/workflow.compression.json');
|
const workflowData = readJsonFileSync('nodes/Compression/test/node/workflow.compression.json');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import fsPromises from 'fs/promises';
|
import fsPromises from 'fs/promises';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { testWorkflows, getWorkflowFilenames, initBinaryDataManager } from '@test/nodes/Helpers';
|
import { testWorkflows, getWorkflowFilenames, initBinaryDataService } from '@test/nodes/Helpers';
|
||||||
|
|
||||||
const workflows = getWorkflowFilenames(__dirname);
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ describe('Test Crypto Node', () => {
|
|||||||
fs.createReadStream = () => Readable.from(Buffer.from('test')) as fs.ReadStream;
|
fs.createReadStream = () => Readable.from(Buffer.from('test')) as fs.ReadStream;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWorkflows(workflows);
|
testWorkflows(workflows);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
equalityTest,
|
equalityTest,
|
||||||
workflowToTests,
|
workflowToTests,
|
||||||
getWorkflowFilenames,
|
getWorkflowFilenames,
|
||||||
initBinaryDataManager,
|
initBinaryDataService,
|
||||||
} from '@test/nodes/Helpers';
|
} from '@test/nodes/Helpers';
|
||||||
|
|
||||||
describe('Test Binary Data Download', () => {
|
describe('Test Binary Data Download', () => {
|
||||||
@@ -14,7 +14,7 @@ describe('Test Binary Data Download', () => {
|
|||||||
const baseUrl = 'https://dummy.domain';
|
const baseUrl = 'https://dummy.domain';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
|
|
||||||
nock.disableNetConnect();
|
nock.disableNetConnect();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { setup, equalityTest, workflowToTests, getWorkflowFilenames } from '@test/nodes/Helpers';
|
import {
|
||||||
|
initBinaryDataService,
|
||||||
|
setup,
|
||||||
|
equalityTest,
|
||||||
|
workflowToTests,
|
||||||
|
getWorkflowFilenames,
|
||||||
|
} from '@test/nodes/Helpers';
|
||||||
|
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
|
|
||||||
@@ -8,7 +14,8 @@ describe('Test HTTP Request Node', () => {
|
|||||||
|
|
||||||
const baseUrl = 'https://dummyjson.com';
|
const baseUrl = 'https://dummyjson.com';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(async () => {
|
||||||
|
await initBinaryDataService();
|
||||||
nock.disableNetConnect();
|
nock.disableNetConnect();
|
||||||
|
|
||||||
//GET
|
//GET
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
getResultNodeData,
|
getResultNodeData,
|
||||||
setup,
|
setup,
|
||||||
readJsonFileSync,
|
readJsonFileSync,
|
||||||
initBinaryDataManager,
|
initBinaryDataService,
|
||||||
} from '@test/nodes/Helpers';
|
} from '@test/nodes/Helpers';
|
||||||
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
|
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
|
||||||
|
|
||||||
describe('Execute iCalendar Node', () => {
|
describe('Execute iCalendar Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
});
|
});
|
||||||
const workflowData = readJsonFileSync('nodes/ICalendar/test/node/workflow.iCalendar.json');
|
const workflowData = readJsonFileSync('nodes/ICalendar/test/node/workflow.iCalendar.json');
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from 'path';
|
|||||||
|
|
||||||
describe('Test Move Binary Data Node', () => {
|
describe('Test Move Binary Data Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Helpers.initBinaryDataManager();
|
await Helpers.initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflow = Helpers.readJsonFileSync(
|
const workflow = Helpers.readJsonFileSync(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import nock from 'nock';
|
|||||||
|
|
||||||
describe('Test QuickChart Node', () => {
|
describe('Test QuickChart Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Helpers.initBinaryDataManager();
|
await Helpers.initBinaryDataService();
|
||||||
nock.disableNetConnect();
|
nock.disableNetConnect();
|
||||||
nock('https://quickchart.io')
|
nock('https://quickchart.io')
|
||||||
.persist()
|
.persist()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from 'path';
|
|||||||
|
|
||||||
describe('Test Read Binary File Node', () => {
|
describe('Test Read Binary File Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Helpers.initBinaryDataManager();
|
await Helpers.initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflow = Helpers.readJsonFileSync(
|
const workflow = Helpers.readJsonFileSync(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from 'path';
|
|||||||
|
|
||||||
describe('Test Read Binary Files Node', () => {
|
describe('Test Read Binary Files Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Helpers.initBinaryDataManager();
|
await Helpers.initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflow = Helpers.readJsonFileSync(
|
const workflow = Helpers.readJsonFileSync(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { getWorkflowFilenames, initBinaryDataManager, testWorkflows } from '@test/nodes/Helpers';
|
import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
|
||||||
|
|
||||||
describe('Test Read PDF Node', () => {
|
describe('Test Read PDF Node', () => {
|
||||||
const workflows = getWorkflowFilenames(__dirname);
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initBinaryDataManager();
|
await initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWorkflows(workflows);
|
testWorkflows(workflows);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from 'path';
|
|||||||
|
|
||||||
describe('Execute Spreadsheet File Node', () => {
|
describe('Execute Spreadsheet File Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Helpers.initBinaryDataManager();
|
await Helpers.initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
// replace workflow json 'Read Binary File' node's filePath to local file
|
// replace workflow json 'Read Binary File' node's filePath to local file
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from 'path';
|
|||||||
|
|
||||||
describe('Test Write Binary File Node', () => {
|
describe('Test Write Binary File Node', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Helpers.initBinaryDataManager();
|
await Helpers.initBinaryDataService();
|
||||||
});
|
});
|
||||||
|
|
||||||
const temporaryDir = Helpers.createTemporaryDir();
|
const temporaryDir = Helpers.createTemporaryDir();
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"snowflake-sdk": "^1.8.0",
|
"snowflake-sdk": "^1.8.0",
|
||||||
"ssh2-sftp-client": "^7.0.0",
|
"ssh2-sftp-client": "^7.0.0",
|
||||||
"tmp-promise": "^3.0.2",
|
"tmp-promise": "^3.0.2",
|
||||||
|
"typedi": "^0.10.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz",
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import path from 'path';
|
|||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { BinaryDataManager, Credentials, constructExecutionMetaData } from 'n8n-core';
|
import { BinaryDataService, Credentials, constructExecutionMetaData } from 'n8n-core';
|
||||||
|
import { Container } from 'typedi';
|
||||||
import type {
|
import type {
|
||||||
CredentialLoadingDetails,
|
CredentialLoadingDetails,
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
@@ -216,14 +217,10 @@ export function createTemporaryDir(prefix = 'n8n') {
|
|||||||
return mkdtempSync(path.join(tmpdir(), prefix));
|
return mkdtempSync(path.join(tmpdir(), prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initBinaryDataManager(mode: 'default' | 'filesystem' = 'default') {
|
export async function initBinaryDataService(mode: 'default' | 'filesystem' = 'default') {
|
||||||
const temporaryDir = createTemporaryDir();
|
const binaryDataService = new BinaryDataService();
|
||||||
await BinaryDataManager.init({
|
await binaryDataService.init({ mode: 'default', availableModes: [mode] });
|
||||||
mode,
|
Container.set(BinaryDataService, binaryDataService);
|
||||||
availableModes: mode,
|
|
||||||
localStoragePath: temporaryDir,
|
|
||||||
});
|
|
||||||
return temporaryDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentialTypes = new CredentialType();
|
const credentialTypes = new CredentialType();
|
||||||
|
|||||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@@ -139,7 +139,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
axios:
|
axios:
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.4
|
version: 0.21.4(debug@4.3.2)
|
||||||
|
|
||||||
packages/@n8n_io/eslint-config:
|
packages/@n8n_io/eslint-config:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -217,7 +217,7 @@ importers:
|
|||||||
version: 7.28.1
|
version: 7.28.1
|
||||||
axios:
|
axios:
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.4
|
version: 0.21.4(debug@4.3.2)
|
||||||
basic-auth:
|
basic-auth:
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
@@ -572,7 +572,7 @@ importers:
|
|||||||
version: link:../@n8n/client-oauth2
|
version: link:../@n8n/client-oauth2
|
||||||
axios:
|
axios:
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.4
|
version: 0.21.4(debug@4.3.2)
|
||||||
concat-stream:
|
concat-stream:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@@ -618,6 +618,9 @@ importers:
|
|||||||
qs:
|
qs:
|
||||||
specifier: ^6.10.1
|
specifier: ^6.10.1
|
||||||
version: 6.11.0
|
version: 6.11.0
|
||||||
|
typedi:
|
||||||
|
specifier: ^0.10.0
|
||||||
|
version: 0.10.0(patch_hash=62r6bc2crgimafeyruodhqlgo4)
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^8.3.2
|
specifier: ^8.3.2
|
||||||
version: 8.3.2
|
version: 8.3.2
|
||||||
@@ -835,7 +838,7 @@ importers:
|
|||||||
version: 10.2.0(vue@3.3.4)
|
version: 10.2.0(vue@3.3.4)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.4
|
version: 0.21.4(debug@4.3.2)
|
||||||
codemirror-lang-html-n8n:
|
codemirror-lang-html-n8n:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -1171,6 +1174,9 @@ importers:
|
|||||||
tmp-promise:
|
tmp-promise:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
typedi:
|
||||||
|
specifier: ^0.10.0
|
||||||
|
version: 0.10.0(patch_hash=62r6bc2crgimafeyruodhqlgo4)
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^8.3.2
|
specifier: ^8.3.2
|
||||||
version: 8.3.2
|
version: 8.3.2
|
||||||
@@ -5016,7 +5022,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@segment/loosely-validate-event': 2.0.0
|
'@segment/loosely-validate-event': 2.0.0
|
||||||
auto-changelog: 1.16.4
|
auto-changelog: 1.16.4
|
||||||
axios: 0.21.4
|
axios: 0.21.4(debug@4.3.2)
|
||||||
axios-retry: 3.3.1
|
axios-retry: 3.3.1
|
||||||
bull: 3.29.3
|
bull: 3.29.3
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
@@ -9066,14 +9072,6 @@ packages:
|
|||||||
is-retry-allowed: 2.2.0
|
is-retry-allowed: 2.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/axios@0.21.4:
|
|
||||||
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: 1.15.2(debug@4.3.4)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/axios@0.21.4(debug@4.3.2):
|
/axios@0.21.4(debug@4.3.2):
|
||||||
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9082,6 +9080,15 @@ packages:
|
|||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/axios@0.27.2:
|
||||||
|
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.2(debug@4.3.2)
|
||||||
|
form-data: 4.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
/axios@0.27.2(debug@3.2.7):
|
/axios@0.27.2(debug@3.2.7):
|
||||||
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
|
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9098,11 +9105,12 @@ packages:
|
|||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
dev: true
|
||||||
|
|
||||||
/axios@1.4.0:
|
/axios@1.4.0:
|
||||||
resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==}
|
resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.2(debug@4.3.4)
|
follow-redirects: 1.15.2(debug@4.3.2)
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
proxy-from-env: 1.1.0
|
proxy-from-env: 1.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -12668,6 +12676,7 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4(supports-color@8.1.1)
|
||||||
|
dev: true
|
||||||
|
|
||||||
/for-each@0.3.3:
|
/for-each@0.3.3:
|
||||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
@@ -17997,7 +18006,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==}
|
resolution: {integrity: sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==}
|
||||||
engines: {node: '>=14.17.0'}
|
engines: {node: '>=14.17.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 0.27.2(debug@4.3.4)
|
axios: 0.27.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
Reference in New Issue
Block a user