Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Integrate object store as binary data manager #7253

Merged
Merged
Show file tree
Hide file tree
Changes from 250 commits
Commits
Show all changes
300 commits
Select commit Hold shift + click to select a range
e8ccf05
Merge branch 'master' into pay-771-implement-soft-deletion
ivov Sep 14, 2023
2071c7f
Remove call from hook
ivov Sep 14, 2023
be44721
Merge parent branch
ivov Sep 14, 2023
1d50b61
Refix conflict
ivov Sep 14, 2023
e042e91
Add formats to schema
ivov Sep 14, 2023
e948f8f
Add `ensureStringArray` util
ivov Sep 14, 2023
0feb926
Adjust types based on parsing
ivov Sep 14, 2023
c73f18c
Add type guard
ivov Sep 14, 2023
f8fec92
Clean up types in `init()`
ivov Sep 14, 2023
93ea57c
Fix test
ivov Sep 14, 2023
c8676be
Remove unneeded modifier
ivov Sep 14, 2023
25006e8
Cleanup
ivov Sep 14, 2023
a7156f8
Fix node tests
ivov Sep 14, 2023
e0e90b6
Remove unneeded line
ivov Sep 14, 2023
219addb
Rename constant
ivov Sep 14, 2023
d7c7f5f
Remove comment
ivov Sep 14, 2023
1ea380d
Fix tests
ivov Sep 14, 2023
a5b388b
Cleanup
ivov Sep 14, 2023
9204754
Rename error
ivov Sep 14, 2023
9597508
Improve error message
ivov Sep 14, 2023
8fde8be
Better error
ivov Sep 14, 2023
ada012e
Better message
ivov Sep 14, 2023
67e163f
Make execution ID non-nullable
ivov Sep 15, 2023
9665da1
Merge master
ivov Sep 15, 2023
12636dd
Readability improvements
ivov Sep 15, 2023
e5c8c72
Adjust types, followup to 67e163f
ivov Sep 15, 2023
8849007
Merge parent branch
ivov Sep 15, 2023
b7062e5
Fix lint
ivov Sep 15, 2023
79ecaa9
Fix lint
ivov Sep 15, 2023
02b6d86
Cleanup
ivov Sep 18, 2023
745437f
Install `aws4` and `@types/aws4`
ivov Sep 18, 2023
ccd3abb
Add to config schema
ivov Sep 18, 2023
afcc474
Set up object store service
ivov Sep 18, 2023
0f909b0
Set up temp dev code
ivov Sep 18, 2023
96ea9ea
Comment out cache keys
ivov Sep 18, 2023
3d76b21
Revert "Comment out cache keys"
ivov Sep 18, 2023
455c327
Rename dir to `TempBinaryData`
ivov Sep 18, 2023
65b4fb0
Rename back to `BinaryData`
ivov Sep 18, 2023
3bf3430
Merge master
ivov Sep 18, 2023
abe6d9d
Clear timers on shutdown
ivov Sep 18, 2023
2682b3d
Set timers only on main instance
ivov Sep 18, 2023
1c788fe
Also for clearing timers
ivov Sep 18, 2023
f248982
Merge parent branch
ivov Sep 18, 2023
04f4b83
Rename back to `localStoragePath`
ivov Sep 18, 2023
03264f9
Remove unused arg
ivov Sep 18, 2023
a6e33d4
Fix lint
ivov Sep 18, 2023
fa09435
Merge parent branch
ivov Sep 18, 2023
552860a
Make as enterprise edition
ivov Sep 18, 2023
8823554
Add logging, refactor for readability
ivov Sep 19, 2023
a5def40
Ensure hard-deletion select includes soft-deleted rows
ivov Sep 19, 2023
77955e4
Switch `info` to `debug`
ivov Sep 19, 2023
b930e3e
Fix tests
ivov Sep 19, 2023
e91f3de
Remove redundant checks for `deletedAt` being `NULL`
ivov Sep 19, 2023
2c3704d
Fix lint
ivov Sep 19, 2023
215f58e
Fix last test
ivov Sep 19, 2023
fabecea
More setup
ivov Sep 19, 2023
33344bc
Add sample path structure
ivov Sep 19, 2023
70ab53b
Set up some errors
ivov Sep 19, 2023
e22f0bf
Remove unused
ivov Sep 19, 2023
d5c8935
Cleanup
ivov Sep 19, 2023
55b6b6f
Fix `HEAD` request
ivov Sep 19, 2023
c33d164
More missing loggers in tests
ivov Sep 19, 2023
69764f7
Add logger to even more test
ivov Sep 19, 2023
6b313ee
Fix upload
ivov Sep 19, 2023
7c3e58d
Refactor logging for tests
ivov Sep 19, 2023
7f6d66a
Consolidate `getStream` and `getBuffer`
ivov Sep 19, 2023
3408a31
Cleanup
ivov Sep 19, 2023
2aeb9ac
Support single deletion
ivov Sep 19, 2023
cc60aeb
Cleanup
ivov Sep 19, 2023
e52ca88
More cleanup
ivov Sep 19, 2023
6fe79b8
Merge parent branch
ivov Sep 19, 2023
eebadfd
Merge parent branch
ivov Sep 19, 2023
6008c41
Remove sample image
ivov Sep 19, 2023
f1b0ca6
Add listing
ivov Sep 19, 2023
d82749b
Implement `deleteMany`
ivov Sep 19, 2023
9e43623
Rename arg
ivov Sep 19, 2023
09ace0c
Major refactor to keep reducing interface
ivov Sep 20, 2023
5810a1b
refactor(core): Include workflow ID in binary data operations
ivov Sep 20, 2023
04584b0
Fix lint
ivov Sep 20, 2023
4f025b2
Merge master, fix conflicts
ivov Sep 20, 2023
80a4fc5
Fix misresolved conflict
ivov Sep 20, 2023
20d5ea7
Remove excess check on init
ivov Sep 20, 2023
eb0849e
Merge parent branch
ivov Sep 20, 2023
f99df11
Fix misresolved conflicts
ivov Sep 20, 2023
4475773
Remove commented out bits
ivov Sep 20, 2023
4362e23
Remove comment
ivov Sep 20, 2023
0714d44
Consistent naming
ivov Sep 20, 2023
7614b6f
Merge parent branch
ivov Sep 20, 2023
fa8e51d
Reorder
ivov Sep 21, 2023
91f0755
Better error message
ivov Sep 21, 2023
6d0c271
Remove `ObjectStore` from this PR
ivov Sep 21, 2023
fa88894
Remove schema change from this PR
ivov Sep 21, 2023
de61768
Update deps
ivov Sep 21, 2023
eb03437
Remove missing dep
ivov Sep 21, 2023
c9e4924
Remove reference
ivov Sep 21, 2023
c9453fd
Cleanup
ivov Sep 21, 2023
3d04b9e
Remove `ObjectStore.manager.ts` stub
ivov Sep 21, 2023
0f84eb6
Remove `ObjectStore.manager.ts` stub
ivov Sep 21, 2023
8d21370
Merge parent branch
ivov Sep 21, 2023
9d3b7e5
Revert changes to lockfile
ivov Sep 21, 2023
a4a915a
Cleanup
ivov Sep 21, 2023
cd501d7
Cleanup
ivov Sep 21, 2023
824fc21
Remove unneeded comment
ivov Sep 21, 2023
875188b
Better naming
ivov Sep 21, 2023
f3a9729
Cleanup
ivov Sep 21, 2023
f85cf8b
Move `ensureDirExists` to utils
ivov Sep 21, 2023
571fe0a
Consolidate errors
ivov Sep 21, 2023
03b3947
Improve typings
ivov Sep 21, 2023
9ab615f
Merge parent branch, resolve conflicts
ivov Sep 21, 2023
594aab1
Fix misresolved conflict
ivov Sep 21, 2023
e190efd
Restore error
ivov Sep 21, 2023
605d02b
Revert rename to `BinaryDataManager`
ivov Sep 21, 2023
922ce32
Merge parent branch
ivov Sep 21, 2023
6e6b28e
Merge parent branch
ivov Sep 21, 2023
b49cb7a
Cleanup
ivov Sep 21, 2023
ba91b05
More accurate naming
ivov Sep 21, 2023
ffc994d
Merge parent branch
ivov Sep 21, 2023
c430f22
Missing renaming in test
ivov Sep 21, 2023
5a603d2
Merge parent branch
ivov Sep 21, 2023
90a85eb
Merge parent branch
ivov Sep 21, 2023
73b38df
Initial setup
ivov Sep 21, 2023
73c0cb3
Add stub for `ObjectStore.manager.ts`
ivov Sep 21, 2023
ed9d514
Tighten typings
ivov Sep 21, 2023
6613f23
Merge parent branch
ivov Sep 21, 2023
f3c8983
Merge parent branch
ivov Sep 21, 2023
0fe0aeb
Mark spots to update
ivov Sep 21, 2023
06002ad
Cleanup
ivov Sep 21, 2023
f144cb9
Simplify
ivov Sep 21, 2023
94e5449
More simplifications
ivov Sep 21, 2023
47d5646
Better naming
ivov Sep 21, 2023
a22818d
Merge parent branch
ivov Sep 21, 2023
1f485da
Simplify stub
ivov Sep 21, 2023
08ae523
Make `getAsStream` async
ivov Sep 21, 2023
2d0325b
Async change also in `WebhookHelpers`
ivov Sep 21, 2023
a1f3d99
Address user-written files
ivov Sep 21, 2023
a1b82ae
Cleanup
ivov Sep 21, 2023
bba6f76
Fix test
ivov Sep 21, 2023
caac8e1
Cleanup
ivov Sep 21, 2023
75eb2f0
Fill out copy and deletion methods
ivov Sep 21, 2023
d79fbe3
Cleanup
ivov Sep 21, 2023
40df3d1
Integrate manager into service
ivov Sep 21, 2023
ac90df9
Add TODO
ivov Sep 21, 2023
06c80c6
Cleanup
ivov Sep 21, 2023
414a357
Better typing
ivov Sep 21, 2023
05034e2
Dyamic imports
ivov Sep 22, 2023
dd6d085
Revert test changes
ivov Sep 22, 2023
fe52383
Add metadata support
ivov Sep 22, 2023
99c0371
Adjust `/data/:path`
ivov Sep 22, 2023
0cdfb79
Rename constant
ivov Sep 22, 2023
08ce3f7
Merge master
ivov Sep 22, 2023
61c2f5a
Restore `LogCatch`
ivov Sep 22, 2023
74adb24
Add file not found error to `getSize`
ivov Sep 22, 2023
0bc2457
Add `mode` to `InvalidBinaryDataManagerError`
ivov Sep 22, 2023
3c96f2f
Merge parent branch
ivov Sep 22, 2023
4c62df6
Resolve more conflicts
ivov Sep 22, 2023
bd3d202
Merge parent branch, fix conflicts
ivov Sep 22, 2023
672d0a5
Merge parent branch, fix conflicts
ivov Sep 22, 2023
3fa72d0
Update per merge
ivov Sep 22, 2023
8a28853
Add breaking change
ivov Sep 22, 2023
c90da67
Refactor `prepareRequestBody`
ivov Sep 22, 2023
fa0f6d0
Remove jest-injected args
ivov Sep 22, 2023
092d212
Await for QS to build
ivov Sep 22, 2023
40c054d
Fix headers in HTTPRN
ivov Sep 22, 2023
0620800
Merge master, fix conflicts
ivov Sep 25, 2023
00e4a87
Remove comment
ivov Sep 25, 2023
410c932
Set order `workflowId -> executionId` in `copyBinaryFile`
ivov Sep 25, 2023
86580c8
Set order `workflowId -> executionId` in `store`
ivov Sep 25, 2023
55062f3
Set order `workflowId -> executionId` in duplication methods
ivov Sep 25, 2023
91e60e1
Cleanup
ivov Sep 25, 2023
f66dfb9
Remove needless casting
ivov Sep 25, 2023
aac2bff
Set order `workflowId -> executionId` in `copyByFileId`
ivov Sep 25, 2023
0d5b425
Add unit tests
ivov Sep 25, 2023
b546a4d
Merge parent branch
ivov Sep 25, 2023
209b024
Add license and schema flags
ivov Sep 25, 2023
c44925e
Merge master, fix conflicts
ivov Sep 25, 2023
de88aeb
Merge master, fix conflicts
ivov Sep 25, 2023
1dfec1f
Merge parent branch
ivov Sep 25, 2023
824ceb4
Fix build
ivov Sep 25, 2023
c79ba57
Remove integration parts
ivov Sep 25, 2023
84596ab
Cleanup
ivov Sep 25, 2023
f556cee
Remove comment
ivov Sep 25, 2023
c27b798
Initial setup
ivov Sep 25, 2023
596ae8d
Merge master
ivov Sep 25, 2023
923499f
Cleanup
ivov Sep 25, 2023
6577851
Minor improvements
ivov Sep 26, 2023
5278f4b
Merge parent branch
ivov Sep 26, 2023
2dbbfa8
Remove from FE settings
ivov Sep 26, 2023
bbddb2a
Remmove from `IN8nUISettings`
ivov Sep 26, 2023
73b09b3
Missing spot
ivov Sep 26, 2023
16503c5
Initialize s3 binary manager
ivov Sep 26, 2023
e3cb832
Add rename
ivov Sep 26, 2023
b2bd541
Fix check
ivov Sep 26, 2023
09d862e
Inject dependency
ivov Sep 26, 2023
2dd42b2
Missing spot
ivov Sep 26, 2023
769550b
Fix serving to UI
ivov Sep 26, 2023
377245b
Fill out comment
ivov Sep 26, 2023
310fa9a
Better naming
ivov Sep 26, 2023
e3f7bf3
Rename env vars
ivov Sep 26, 2023
145bce5
Remove `deleteOne`
ivov Sep 26, 2023
89919a1
Fix tests
ivov Sep 26, 2023
98d0c3a
Rename errors
ivov Sep 26, 2023
63c03d5
Straighten out license checks
ivov Sep 26, 2023
d1ae8fa
Fix tests
ivov Sep 26, 2023
157d2ef
Adjust license checks
ivov Sep 26, 2023
7f65303
Add explanatory comments
ivov Sep 26, 2023
b92d6d7
Merge master, fix conflicts
ivov Sep 26, 2023
a3ecaed
Merge parent branch, fix conflicts
ivov Sep 26, 2023
24cc474
Add readonly mode
ivov Sep 26, 2023
4afcb19
Implement `copyByFilePath`
ivov Sep 26, 2023
537bb0c
Remove logging
ivov Sep 26, 2023
63b2db2
Adjust `restoreBinaryDataId` for s3
ivov Sep 26, 2023
8dcc087
Write docs
ivov Sep 26, 2023
8575f49
Write unit tests
ivov Sep 27, 2023
7e7902c
Merge master, fix conflicts
ivov Sep 27, 2023
a444552
Typo
ivov Sep 27, 2023
5af3015
Better naming
ivov Sep 27, 2023
5aeb541
Cleanup
ivov Sep 27, 2023
d768250
Remove `inTest`
ivov Sep 27, 2023
37cbdb9
Add TODO
ivov Sep 27, 2023
d53a2e2
Account for license expiring when blocking writes
ivov Sep 27, 2023
b6363c8
Clarifications
ivov Sep 27, 2023
da4c118
Update setting on doc
ivov Sep 27, 2023
5873b93
Parameterize host
ivov Sep 27, 2023
9c48a7b
Switch pattern from bucket before host to bucket in path
ivov Sep 27, 2023
a2f67dd
Cleanup
ivov Sep 27, 2023
5b27a9b
Add example
ivov Sep 27, 2023
9c5d2be
Avoid checking connection twice
ivov Sep 27, 2023
75d66f9
Remove logging
ivov Sep 27, 2023
c1e8fdf
Fix tests
ivov Sep 27, 2023
889c8d3
Add logging
ivov Sep 27, 2023
05cea35
Remove logging
ivov Sep 27, 2023
9b61f85
Fix tests
ivov Sep 27, 2023
bc90500
Cleanup
ivov Sep 28, 2023
258f218
Add more logging
ivov Sep 28, 2023
07d2b1b
Fix lint
ivov Sep 28, 2023
b75cbbb
Improve check
ivov Sep 28, 2023
9fde3f0
Simplify check
ivov Sep 28, 2023
2a3c1e2
Add examples
ivov Sep 28, 2023
f98a467
Typo
ivov Sep 28, 2023
a398719
Refactor test
ivov Sep 28, 2023
e584be1
Expand tests
ivov Sep 28, 2023
39b3188
Improve error message
ivov Sep 28, 2023
fe56d4e
Inject dependency
ivov Sep 28, 2023
05f2467
Comment to clarify
ivov Sep 28, 2023
d4bb172
Merge master
ivov Sep 28, 2023
02a54da
Refactor startup logic
ivov Sep 28, 2023
a8c39ed
Remove setup doc
ivov Oct 2, 2023
4e99a90
Rename envs
ivov Oct 2, 2023
deb6076
Remove Axios error from log
ivov Oct 2, 2023
7a0039c
Better var naming
ivov Oct 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/cli/src/License.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Container, { Service } from 'typedi';
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
import { RedisService } from './services/redis.service';
import { ObjectStoreService } from 'n8n-core';

type FeatureReturnType = Partial<
{
Expand Down Expand Up @@ -103,6 +104,18 @@ export class License {
command: 'reloadLicense',
});
}

const isSelected = config.getEnv('binaryDataManager.mode') === 's3';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of the naming here since it does not show relation to the binary data mode and onfeaturechange() is a general function

const isAvailable = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isLicensed = _features['feat:binaryDataS3'];

if (isSelected && isAvailable && !isLicensed) {
this.logger.debug(
'License changed with no support for external storage - blocking writes on object store. To restore writes, please upgrade to a license that supports this feature.',
);

Container.get(ObjectStoreService).setReadonly(true);
}
}

async saveCertStr(value: TLicenseBlock): Promise<void> {
Expand Down
29 changes: 20 additions & 9 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1428,28 +1428,39 @@ export class Server extends AbstractServer {
// Binary data
// ----------------------------------------

// Download binary
// View or download binary file
this.app.get(
`/${this.restEndpoint}/data/:path`,
`/${this.restEndpoint}/data`,
async (req: BinaryDataRequest, res: express.Response): Promise<void> => {
// TODO UM: check if this needs permission check for UM
const identifier = req.params.path;
const { id: binaryDataId, action } = req.query;
let { fileName, mimeType } = req.query;
const [mode] = binaryDataId.split(':') as ['filesystem' | 's3', string];

try {
const binaryPath = this.binaryDataService.getPath(identifier);
let { mode, fileName, mimeType } = req.query;
const binaryPath = this.binaryDataService.getPath(binaryDataId);

if (!fileName || !mimeType) {
try {
const metadata = await this.binaryDataService.getMetadata(identifier);
const metadata = await this.binaryDataService.getMetadata(binaryDataId);
fileName = metadata.fileName;
mimeType = metadata.mimeType;
res.setHeader('Content-Length', metadata.fileSize);
} catch {}
}

if (mimeType) res.setHeader('Content-Type', mimeType);
if (mode === 'download') {

if (action === 'download') {
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
}
res.sendFile(binaryPath);

if (mode === 's3') {
const readStream = await this.binaryDataService.getAsStream(binaryDataId);
readStream.pipe(res);
return;
} else {
res.sendFile(binaryPath);
}
} catch (error) {
if (error instanceof FileNotFoundError) res.writeHead(404).end();
else throw error;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/WorkflowExecuteAdditionalData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
workflowId: this.workflowData.id,
});

if (this.mode === 'webhook' && config.getEnv('binaryDataManager.mode') === 'filesystem') {
if (this.mode === 'webhook' && config.getEnv('binaryDataManager.mode') !== 'default') {
await restoreBinaryDataId(fullRunData, this.executionId);
}

Expand Down
116 changes: 114 additions & 2 deletions packages/cli/src/commands/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { ExitError } from '@oclif/errors';
import { Container } from 'typedi';
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import type { IUserSettings } from 'n8n-core';
import { BinaryDataService, UserSettings } from 'n8n-core';
import { BinaryDataService, ObjectStoreService, UserSettings } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer';
import { getLogger } from '@/Logger';
import config from '@/config';
import * as Db from '@/Db';
import * as CrashJournal from '@/CrashJournal';
import { inTest } from '@/constants';
import { LICENSE_FEATURES, inTest } from '@/constants';
import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { initErrorHandling } from '@/ErrorReporting';
Expand Down Expand Up @@ -126,7 +126,119 @@ export abstract class BaseCommand extends Command {
process.exit(1);
}

async initObjectStoreService() {
const isSelected = config.getEnv('binaryDataManager.mode') === 's3';
const isAvailable = config.getEnv('binaryDataManager.availableModes').includes('s3');

if (!isSelected && !isAvailable) return;

if (isSelected && !isAvailable) {
throw new Error(
'External storage selected but unavailable. Please make external storage available by adding "s3" to `N8N_AVAILABLE_BINARY_DATA_MODES`.',
);
}

const isLicensed = Container.get(License).isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);

if (isSelected && isAvailable && isLicensed) {
LoggerProxy.debug(
'License found for external storage - object store to init in read-write mode',
);

await this._initObjectStoreService();

return;
}

if (isSelected && isAvailable && !isLicensed) {
LoggerProxy.debug(
'No license found for external storage - object store to init with writes blocked. To enable writes, please upgrade to a license that supports this feature.',
);

await this._initObjectStoreService({ isReadOnly: true });

return;
}

if (!isSelected && isAvailable) {
LoggerProxy.debug(
'External storage unselected but available - object store to init with writes unused',
);

await this._initObjectStoreService();

return;
}
}

private async _initObjectStoreService(options = { isReadOnly: false }) {
const objectStoreService = Container.get(ObjectStoreService);

const host = config.getEnv('externalStorage.s3.host');

if (host === '') {
throw new Error(
'External storage host not configured. Please set `N8N_EXTERNAL_STORAGE_S3_HOST`.',
);
}

const bucket = {
name: config.getEnv('externalStorage.s3.bucket.name'),
region: config.getEnv('externalStorage.s3.bucket.region'),
};

if (bucket.name === '') {
throw new Error(
'External storage bucket name not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME`.',
);
}

if (bucket.region === '') {
throw new Error(
'External storage bucket region not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION`.',
);
}

const credentials = {
accountId: config.getEnv('externalStorage.s3.credentials.accountId'),
secretKey: config.getEnv('externalStorage.s3.credentials.secretKey'),
};

if (credentials.accountId === '') {
throw new Error(
'External storage account ID not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCOUNT_ID`.',
);
}

if (credentials.secretKey === '') {
throw new Error(
'External storage secret key not configured. Please set `N8N_EXTERNAL_STORAGE_S3_SECRET_KEY`.',
);
}

LoggerProxy.debug('Initializing object store service');

try {
await objectStoreService.init(host, bucket, credentials);
objectStoreService.setReadonly(options.isReadOnly);

LoggerProxy.debug('Object store init completed');
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);

LoggerProxy.debug('Object store init failed', { error });
}
}

async initBinaryDataService() {
try {
await this.initObjectStoreService();
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);
LoggerProxy.error(`Failed to init object store: ${error.message}`, { error });
process.exit(1);
}

const binaryDataConfig = config.getEnv('binaryDataManager');
await Container.get(BinaryDataService).init(binaryDataConfig);
}
Expand Down
41 changes: 40 additions & 1 deletion packages/cli/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ export const schema = {
doc: 'Available modes of binary data storage, as comma separated strings',
},
mode: {
format: ['default', 'filesystem'] as const,
format: ['default', 'filesystem', 's3'] as const,
default: 'default',
env: 'N8N_DEFAULT_BINARY_DATA_MODE',
doc: 'Storage mode for binary data',
Expand All @@ -914,6 +914,45 @@ export const schema = {
},
},

externalStorage: {
s3: {
host: {
format: String,
default: '',
env: 'N8N_EXTERNAL_STORAGE_S3_HOST',
doc: 'Host of the n8n bucket in S3-compatible external storage, e.g. `s3.us-east-1.amazonaws.com`',
},
bucket: {
name: {
format: String,
default: '',
env: 'N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME',
doc: 'Name of the n8n bucket in S3-compatible external storage',
},
region: {
format: String,
default: '',
env: 'N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION',
doc: 'Region of the n8n bucket in S3-compatible external storage, e.g. `us-east-1`',
},
},
credentials: {
accountId: {
format: String,
default: '',
env: 'N8N_EXTERNAL_STORAGE_S3_ACCOUNT_ID',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turned out a bit misleading - I tried using the AWS account ID but what's supposed to be here is the access key. I think we should rename this to access key and access secret WDYT?

doc: 'Account ID in S3-compatible external storage',
},
secretKey: {
format: String,
default: '',
env: 'N8N_EXTERNAL_STORAGE_S3_SECRET_KEY',
doc: 'Secret key in S3-compatible external storage',
},
},
},
},

deployment: {
type: {
format: String,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const LICENSE_FEATURES = {
SHOW_NON_PROD_BANNER: 'feat:showNonProdBanner',
WORKFLOW_HISTORY: 'feat:workflowHistory',
DEBUG_IN_EDITOR: 'feat:debugInEditor',
BINARY_DATA_S3: 'feat:binaryDataS3',
} as const;

export const LICENSE_QUOTAS = {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/controllers/e2e.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class E2EController {
[LICENSE_FEATURES.SHOW_NON_PROD_BANNER]: false,
[LICENSE_FEATURES.WORKFLOW_HISTORY]: false,
[LICENSE_FEATURES.DEBUG_IN_EDITOR]: false,
[LICENSE_FEATURES.BINARY_DATA_S3]: false,
};

constructor(
Expand Down
33 changes: 21 additions & 12 deletions packages/cli/src/executionLifecycleHooks/restoreBinaryDataId.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Container from 'typedi';
import { BinaryDataService } from 'n8n-core';
import type { IRun } from 'n8n-workflow';

export function isMissingExecutionId(binaryDataId: string) {
const UUID_CHAR_LENGTH = 36;

return [UUID_CHAR_LENGTH + 'filesystem:'.length, UUID_CHAR_LENGTH + 's3:'.length].some(
(incorrectLength) => binaryDataId.length === incorrectLength,
);
import type { BinaryData } from 'n8n-core';

export function isMissingExecutionId(
fileId: string,
mode: BinaryData.NonDefaultMode,
uuidV4CharLength = 36,
) {
return mode === 'filesystem' ? uuidV4CharLength === fileId.length : fileId.includes('/temp/');
}

/**
Expand All @@ -19,6 +20,9 @@ export function isMissingExecutionId(binaryDataId: string) {
* ```txt
* filesystem:11869055-83c4-4493-876a-9092c4708b9b ->
* filesystem:39011869055-83c4-4493-876a-9092c4708b9b
*
* s3:workflows/123/executions/temp/binary_data/69055-83c4-4493-876a-9092c4708b9b ->
* s3:workflows/123/executions/390/binary_data/69055-83c4-4493-876a-9092c4708b9b
* ```
*/
export async function restoreBinaryDataId(run: IRun, executionId: string) {
Expand All @@ -27,13 +31,18 @@ export async function restoreBinaryDataId(run: IRun, executionId: string) {
const promises = Object.keys(runData).map(async (nodeName) => {
const binaryDataId = runData[nodeName]?.[0]?.data?.main?.[0]?.[0].binary?.data.id;

if (!binaryDataId || !isMissingExecutionId(binaryDataId)) return;
if (!binaryDataId) return;

const [mode, incorrectFileId] = binaryDataId.split(':');
const correctFileId = `${executionId}${incorrectFileId}`;
const correctBinaryDataId = `${mode}:${correctFileId}`;
const [mode, fileId] = binaryDataId.split(':') as [BinaryData.NonDefaultMode, string];

await Container.get(BinaryDataService).rename(incorrectFileId, correctFileId);
if (!isMissingExecutionId(fileId, mode)) return;

const correctFileId =
mode === 'filesystem' ? `${executionId}${fileId}` : fileId.replace('temp', executionId);

await Container.get(BinaryDataService).rename(fileId, correctFileId);

const correctBinaryDataId = `${mode}:${correctFileId}`;

// @ts-expect-error Validated at the top
run.data.resultData.runData[nodeName][0].data.main[0][0].binary.data.id = correctBinaryDataId;
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,11 +492,12 @@ export declare namespace LicenseRequest {
}

export type BinaryDataRequest = AuthenticatedRequest<
{ path: string },
{},
{},
{},
{
mode: 'view' | 'download';
id: string;
action: 'view' | 'download';
fileName?: string;
mimeType?: string;
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/test/integration/commands/worker.cmd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const oclifConfig: Config.IConfig = new Config.Config({ root: __dirname });
beforeAll(async () => {
LoggerProxy.init(getLogger());
config.set('executions.mode', 'queue');
config.set('binaryDataManager.availableModes', 'filesystem');
mockInstance(Telemetry);
mockInstance(PostHogClient);
mockInstance(InternalHooks);
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/test/integration/shared/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ export async function initNodeTypes() {
/**
* Initialize a BinaryDataService for test runs.
*/
export async function initBinaryDataService() {
export async function initBinaryDataService(mode: 'default' | 'filesystem' = 'default') {
const binaryDataService = new BinaryDataService();

await binaryDataService.init(config.getEnv('binaryDataManager'));

await binaryDataService.init({
mode,
availableModes: [mode],
localStoragePath: '',
});
Container.set(BinaryDataService, binaryDataService);
}

Expand Down
Loading
Loading