Files
Automata/packages/core/src/BinaryDataManager/FileSystem.ts
Iván Ovejero e8e44f6b6e refactor(core): Log binary data file write errors (no-changelog) (#7237)
This PR adds logging for binary data file write errors, to capture why
executions sometimes point to non-existing binary data files. See
[Sentry
error](https://n8nio.sentry.io/issues/4495134693/?alert_rule_id=14556563&alert_type=issue&notification_uuid=4b50a5da-6ae9-472e-9658-984cca824762&project=4503924908883968&referrer=slack).
2023-09-22 11:48:20 +02:00

139 lines
4.3 KiB
TypeScript

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;
}
}