import { Config, Env, ExecutionsConfig } from '@n8n/config'; import { createHash } from 'node:crypto'; import { z } from 'zod'; import { InstanceSettings, type DeploymentStateRepo } from '@/instance-settings'; import { StorageConfig } from '@/storage.config'; export const BINARY_DATA_MODES = ['default', 'filesystem', 's3', 'azure', 'database'] as const; const binaryDataModesSchema = z.enum(BINARY_DATA_MODES); const availableModesSchema = z .string() .transform((value) => value.split(',')) .pipe(binaryDataModesSchema.array()); const dbMaxFileSizeSchema = z.coerce .number() .max(1024, 'Binary data max file size in `database` mode cannot exceed 1024 MiB'); // because of Postgres BYTEA hard limit @Config export class BinaryDataConfig { /** Available modes of binary data storage, as comma separated strings. */ availableModes: z.infer = ['filesystem', 's3', 'database']; /** Storage mode for binary data. Defaults to 'filesystem' in regular mode, 'database' in scaling mode. */ @Env('N8N_DEFAULT_BINARY_DATA_MODE', binaryDataModesSchema) mode!: z.infer; /** 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; /** Maximum file size (in MiB) for binary data in `database` mode. **/ @Env('N8N_BINARY_DATA_DATABASE_MAX_FILE_SIZE', dbMaxFileSizeSchema) dbMaxFileSize: number = 512; constructor( { encryptionKey }: InstanceSettings, executionsConfig: ExecutionsConfig, storageConfig: StorageConfig, ) { /** * Set the binary data storage path: * * - N8N_BINARY_DATA_STORAGE_PATH, else * - N8N_STORAGE_PATH, else * - ~/.n8n/storage * * `~/.n8n/binaryData` is no longer the default if the env var is unset. */ this.localStoragePath ??= storageConfig.storagePath; this.signingSecret = createHash('sha256') .update(`url-signing:${encryptionKey}`) .digest('base64'); this.mode ??= executionsConfig.mode === 'queue' ? 'database' : 'filesystem'; } /** * Two-phase init: reads or creates the signing.binary_data deployment-key row. * Must be called after DB migrations complete, before any signed-URL generation. * Precedence: N8N_BINARY_DATA_SIGNING_SECRET env → DB active row → derive-from-key (and persist) */ async initialize( repo: Pick, ): Promise { if (process.env.N8N_BINARY_DATA_SIGNING_SECRET) { return; } const existing = await repo.findActiveSigningSecret('signing.binary_data', { rewrapLegacy: true, }); if (existing !== null) { this.signingSecret = existing; return; } await repo.seedSigningSecret('signing.binary_data', this.signingSecret); // The winner may be a pre-wrap row inserted concurrently by an older // process — rewrap on this read too, so startup always leaves it wrapped. const winner = await repo.findActiveSigningSecret('signing.binary_data', { rewrapLegacy: true, }); if (winner !== null) this.signingSecret = winner; } }