mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +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:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user