mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
refactor(core): Migrate binary-data config to a decorated config class (#14616)
This commit is contained in:
committed by
GitHub
parent
a12c9522d5
commit
2ca742cb15
@@ -2,8 +2,6 @@ import { mock } from 'jest-mock-extended';
|
||||
import { sign, JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
||||
import type { IBinaryData } from 'n8n-workflow';
|
||||
|
||||
import { type InstanceSettings } from '@/instance-settings';
|
||||
|
||||
import type { BinaryDataConfig } from '../binary-data.config';
|
||||
import { BinaryDataService } from '../binary-data.service';
|
||||
|
||||
@@ -13,7 +11,6 @@ jest.useFakeTimers({ now });
|
||||
describe('BinaryDataService', () => {
|
||||
const signingSecret = 'test-signing-secret';
|
||||
const config = mock<BinaryDataConfig>({ signingSecret });
|
||||
const instanceSettings = mock<InstanceSettings>({ encryptionKey: 'test-encryption-key' });
|
||||
const binaryData = mock<IBinaryData>({ id: 'filesystem:id_123' });
|
||||
const validToken = sign({ id: binaryData.id }, signingSecret, { expiresIn: '1 day' });
|
||||
|
||||
@@ -22,23 +19,7 @@ describe('BinaryDataService', () => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
config.signingSecret = signingSecret;
|
||||
service = new BinaryDataService(instanceSettings, config);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should derive the signingSecret from the encryption-key, if not provided via BinaryDataConfig', () => {
|
||||
config.signingSecret = undefined;
|
||||
|
||||
const service = new BinaryDataService(instanceSettings, config);
|
||||
|
||||
expect(service.signingSecret).toBe(
|
||||
'f7a78761c5cc17a2753e7e9d85d90e12de87d8131aea4479a11d1c7bb9655b20',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use signingSecret as provided in BinaryDataConfig', () => {
|
||||
expect(service.signingSecret).toBe(signingSecret);
|
||||
});
|
||||
service = new BinaryDataService(config);
|
||||
});
|
||||
|
||||
describe('createSignedToken', () => {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { InstanceSettings } from '@/instance-settings';
|
||||
import { mockInstance } from '@test/utils';
|
||||
|
||||
import { BinaryDataConfig } from '../binary-data.config';
|
||||
|
||||
describe('BinaryDataConfig', () => {
|
||||
const n8nFolder = '/test/n8n';
|
||||
const encryptionKey = 'test-encryption-key';
|
||||
console.warn = jest.fn().mockImplementation(() => {});
|
||||
|
||||
const now = new Date('2025-01-01T01:23:45.678Z');
|
||||
jest.useFakeTimers({ now });
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = {};
|
||||
jest.resetAllMocks();
|
||||
Container.reset();
|
||||
mockInstance(InstanceSettings, { encryptionKey, n8nFolder });
|
||||
});
|
||||
|
||||
it('should use default values when no env variables are defined', () => {
|
||||
const config = Container.get(BinaryDataConfig);
|
||||
|
||||
expect(config.availableModes).toEqual(['filesystem']);
|
||||
expect(config.mode).toBe('default');
|
||||
expect(config.localStoragePath).toBe('/test/n8n/binaryData');
|
||||
});
|
||||
|
||||
it('should use values from env variables when defined', () => {
|
||||
process.env.N8N_AVAILABLE_BINARY_DATA_MODES = 'filesystem,s3';
|
||||
process.env.N8N_DEFAULT_BINARY_DATA_MODE = 's3';
|
||||
process.env.N8N_BINARY_DATA_STORAGE_PATH = '/custom/storage/path';
|
||||
process.env.N8N_BINARY_DATA_SIGNING_SECRET = 'super-secret';
|
||||
|
||||
const config = Container.get(BinaryDataConfig);
|
||||
|
||||
expect(config.mode).toEqual('s3');
|
||||
expect(config.availableModes).toEqual(['filesystem', 's3']);
|
||||
expect(config.localStoragePath).toEqual('/custom/storage/path');
|
||||
expect(config.signingSecret).toBe('super-secret');
|
||||
});
|
||||
|
||||
it('should derive the signing secret from the encryption-key, when none is passed in', () => {
|
||||
const config = Container.get(BinaryDataConfig);
|
||||
|
||||
expect(config.signingSecret).toBe('96eHYcXMF6J1Pn6dhdkOEt6H2BMa6kR5oR0ce7llWyA=');
|
||||
});
|
||||
|
||||
it('should fallback to default for mode', () => {
|
||||
process.env.N8N_DEFAULT_BINARY_DATA_MODE = 'invalid-mode';
|
||||
|
||||
const config = Container.get(BinaryDataConfig);
|
||||
|
||||
expect(config.mode).toEqual('default');
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid value for N8N_DEFAULT_BINARY_DATA_MODE'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to default for available modes', () => {
|
||||
process.env.N8N_AVAILABLE_BINARY_DATA_MODES = 'filesystem,invalid-mode,s3';
|
||||
|
||||
const config = Container.get(BinaryDataConfig);
|
||||
|
||||
expect(config.availableModes).toEqual(['filesystem']);
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid value for N8N_AVAILABLE_BINARY_DATA_MODES'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,42 @@
|
||||
import { Config, Env } from '@n8n/config';
|
||||
import { createHash } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { InstanceSettings } from '@/instance-settings';
|
||||
|
||||
const binaryDataModesSchema = z.enum(['default', 'filesystem', 's3']);
|
||||
|
||||
const availableModesSchema = z
|
||||
.string()
|
||||
.transform((value) => value.split(','))
|
||||
.pipe(binaryDataModesSchema.array());
|
||||
|
||||
@Config
|
||||
export class BinaryDataConfig {
|
||||
/** Secret for creating publicly-accesible signed URLs for binary data */
|
||||
/** Available modes of binary data storage, as comma separated strings. */
|
||||
@Env('N8N_AVAILABLE_BINARY_DATA_MODES', availableModesSchema)
|
||||
availableModes: z.infer<typeof availableModesSchema> = ['filesystem'];
|
||||
|
||||
/** Storage mode for binary data. */
|
||||
@Env('N8N_DEFAULT_BINARY_DATA_MODE', binaryDataModesSchema)
|
||||
mode: z.infer<typeof binaryDataModesSchema> = 'default';
|
||||
|
||||
/** Path for binary data storage in "filesystem" mode. */
|
||||
@Env('N8N_BINARY_DATA_STORAGE_PATH')
|
||||
localStoragePath: string;
|
||||
|
||||
/**
|
||||
* Secret for creating publicly-accesible signed URLs for binary data.
|
||||
* When not passed in, this will be derived from the instances's encryption-key
|
||||
**/
|
||||
@Env('N8N_BINARY_DATA_SIGNING_SECRET')
|
||||
signingSecret?: string = undefined;
|
||||
signingSecret: string;
|
||||
|
||||
constructor({ encryptionKey, n8nFolder }: InstanceSettings) {
|
||||
this.localStoragePath = path.join(n8nFolder, 'binaryData');
|
||||
this.signingSecret = createHash('sha256')
|
||||
.update(`url-signing:${encryptionKey}`)
|
||||
.digest('base64');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { createHash } from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { BINARY_ENCODING, UnexpectedError } from 'n8n-workflow';
|
||||
import type { INodeExecutionData, IBinaryData } from 'n8n-workflow';
|
||||
@@ -7,8 +6,6 @@ import { readFile, stat } from 'node:fs/promises';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
import { InstanceSettings } from '@/instance-settings';
|
||||
|
||||
import { BinaryDataConfig } from './binary-data.config';
|
||||
import type { BinaryData } from './types';
|
||||
import { areConfigModes, binaryToBuffer } from './utils';
|
||||
@@ -21,15 +18,10 @@ export class BinaryDataService {
|
||||
|
||||
private managers: Record<string, BinaryData.Manager> = {};
|
||||
|
||||
readonly signingSecret: string;
|
||||
constructor(private readonly config: BinaryDataConfig) {}
|
||||
|
||||
constructor({ encryptionKey }: InstanceSettings, binaryDataConfig: BinaryDataConfig) {
|
||||
this.signingSecret =
|
||||
binaryDataConfig.signingSecret ??
|
||||
createHash('sha256').update(`url-signing:${encryptionKey}`).digest('hex');
|
||||
}
|
||||
|
||||
async init(config: BinaryData.Config) {
|
||||
async init() {
|
||||
const { config } = this;
|
||||
if (!areConfigModes(config.availableModes)) throw new InvalidModeError();
|
||||
|
||||
this.mode = config.mode === 'filesystem' ? 'filesystem-v2' : config.mode;
|
||||
@@ -62,11 +54,13 @@ export class BinaryDataService {
|
||||
id: binaryData.id,
|
||||
};
|
||||
|
||||
return jwt.sign(signingPayload, this.signingSecret, { expiresIn });
|
||||
const { signingSecret } = this.config;
|
||||
return jwt.sign(signingPayload, signingSecret, { expiresIn });
|
||||
}
|
||||
|
||||
validateSignedToken(token: string) {
|
||||
const signedPayload = jwt.verify(token, this.signingSecret) as BinaryData.SigningPayload;
|
||||
const { signingSecret } = this.config;
|
||||
const signedPayload = jwt.verify(token, signingSecret) as BinaryData.SigningPayload;
|
||||
return signedPayload.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './binary-data.service';
|
||||
export { BinaryDataConfig } from './binary-data.config';
|
||||
export * from './types';
|
||||
export { ObjectStoreService } from './object-store/object-store.service.ee';
|
||||
export { isStoredMode as isValidNonDefaultMode } from './utils';
|
||||
|
||||
@@ -22,12 +22,6 @@ export namespace BinaryData {
|
||||
*/
|
||||
export type StoredMode = Exclude<ConfigMode | UpgradedMode, 'default'>;
|
||||
|
||||
export type Config = {
|
||||
mode: ConfigMode;
|
||||
availableModes: ConfigMode[];
|
||||
localStoragePath: string;
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
fileName?: string;
|
||||
mimeType?: string;
|
||||
|
||||
Reference in New Issue
Block a user