Skip to content

Commit

Permalink
refactor(core): Consolidate binary file not found errors (no-changelo…
Browse files Browse the repository at this point in the history
…g) (#7585)

Logging was originally to see if there was a binary data file failing to
be written for [this
user](https://linear.app/n8n/issue/PAY-844/filesystem-binary-data-mode-causing-alerts-in-cloud)
but the cause was not a file failed to be written but a missing `fileId`
in a binary data item in an execution. The error should no longer be
thrown as of 1.12. See story for more info.

This PR is for cleanup and to consolidate any file not found errors in
the context of binary data, to track if this happens again.
  • Loading branch information
ivov authored Nov 3, 2023
1 parent e6d3d1a commit 6d42fad
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 45 deletions.
10 changes: 2 additions & 8 deletions packages/core/src/BinaryData/BinaryData.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { readFile, stat } from 'node:fs/promises';
import prettyBytes from 'pretty-bytes';
import Container, { Service } from 'typedi';
import { BINARY_ENCODING, LoggerProxy as Logger, IBinaryData } from 'n8n-workflow';
import { BINARY_ENCODING } from 'n8n-workflow';
import { UnknownManagerError, InvalidModeError } from './errors';
import { areConfigModes, toBuffer } from './utils';
import { LogCatch } from '../decorators/LogCatch.decorator';

import type { Readable } from 'stream';
import type { BinaryData } from './types';
import type { INodeExecutionData } from 'n8n-workflow';
import type { INodeExecutionData, IBinaryData } from 'n8n-workflow';

@Service()
export class BinaryDataService {
Expand Down Expand Up @@ -40,7 +39,6 @@ export class BinaryDataService {
}
}

@LogCatch((error) => Logger.error('Failed to copy binary data file', { error }))
async copyBinaryFile(
workflowId: string,
executionId: string,
Expand Down Expand Up @@ -76,7 +74,6 @@ export class BinaryDataService {
return binaryData;
}

@LogCatch((error) => Logger.error('Failed to write binary data file', { error }))
async store(
workflowId: string,
executionId: string,
Expand Down Expand Up @@ -152,9 +149,6 @@ export class BinaryDataService {
if (manager.deleteMany) await manager.deleteMany(ids);
}

@LogCatch((error) =>
Logger.error('Failed to copy all binary data files for execution', { error }),
)
async duplicateBinaryData(
workflowId: string,
executionId: string,
Expand Down
20 changes: 12 additions & 8 deletions packages/core/src/BinaryData/FileSystem.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import { v4 as uuid } from 'uuid';
import { jsonParse } from 'n8n-workflow';
import { assertDir } from './utils';
import { FileNotFoundError } from '../errors';
import { assertDir, doesNotExist } from './utils';
import { BinaryFileNotFoundError, InvalidPathError } from '../errors';

import type { Readable } from 'stream';
import type { BinaryData } from './types';
Expand Down Expand Up @@ -46,17 +46,21 @@ export class FileSystemManager implements BinaryData.Manager {
async getAsStream(fileId: string, chunkSize?: number) {
const filePath = this.resolvePath(fileId);

if (await doesNotExist(filePath)) {
throw new BinaryFileNotFoundError(filePath);
}

return createReadStream(filePath, { highWaterMark: chunkSize });
}

async getAsBuffer(fileId: string) {
const filePath = this.resolvePath(fileId);

try {
return await fs.readFile(filePath);
} catch {
throw new Error(`Error finding file: ${filePath}`);
if (await doesNotExist(filePath)) {
throw new BinaryFileNotFoundError(filePath);
}

return fs.readFile(filePath);
}

async getMetadata(fileId: string): Promise<BinaryData.Metadata> {
Expand Down Expand Up @@ -167,7 +171,7 @@ export class FileSystemManager implements BinaryData.Manager {
const returnPath = path.join(this.storagePath, ...args);

if (path.relative(this.storagePath, returnPath).startsWith('..')) {
throw new FileNotFoundError('Invalid path detected');
throw new InvalidPathError(returnPath);
}

return returnPath;
Expand All @@ -186,7 +190,7 @@ export class FileSystemManager implements BinaryData.Manager {
const stats = await fs.stat(filePath);
return stats.size;
} catch (error) {
throw new Error('Failed to find binary data file in filesystem', { cause: error });
throw new BinaryFileNotFoundError(filePath);
}
}
}
9 changes: 9 additions & 0 deletions packages/core/src/BinaryData/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export async function assertDir(dir: string) {
}
}

export async function doesNotExist(dir: string) {
try {
await fs.access(dir);
return false;
} catch {
return true;
}
}

export async function toBuffer(body: Buffer | Readable) {
return new Promise<Buffer>((resolve) => {
if (Buffer.isBuffer(body)) resolve(body);
Expand Down
29 changes: 0 additions & 29 deletions packages/core/src/decorators/LogCatch.decorator.ts

This file was deleted.

8 changes: 8 additions & 0 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ export class FileNotFoundError extends Error {
super(`File not found: ${filePath}`);
}
}

export class BinaryFileNotFoundError extends FileNotFoundError {}

export class InvalidPathError extends Error {
constructor(readonly filePath: string) {
super(`Invalid path detected: ${filePath}`);
}
}
8 changes: 8 additions & 0 deletions packages/core/test/FileSystem.manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('getPath()', () => {
describe('getAsBuffer()', () => {
it('should return a buffer', async () => {
fsp.readFile = jest.fn().mockResolvedValue(mockBuffer);
fsp.access = jest.fn().mockImplementation(async () => {});

const result = await fsManager.getAsBuffer(fileId);

Expand All @@ -64,6 +65,7 @@ describe('getAsBuffer()', () => {
describe('getAsStream()', () => {
it('should return a stream', async () => {
fs.createReadStream = jest.fn().mockReturnValue(mockStream);
fsp.access = jest.fn().mockImplementation(async () => {});

const stream = await fsManager.getAsStream(fileId);

Expand Down Expand Up @@ -123,6 +125,7 @@ describe('copyByFilePath()', () => {
const targetPath = toFullFilePath(otherFileId);

fsp.cp = jest.fn().mockResolvedValue(undefined);
fsp.writeFile = jest.fn().mockResolvedValue(undefined);

const result = await fsManager.copyByFilePath(
workflowId,
Expand All @@ -132,6 +135,11 @@ describe('copyByFilePath()', () => {
);

expect(fsp.cp).toHaveBeenCalledWith(sourceFilePath, targetPath);
expect(fsp.writeFile).toHaveBeenCalledWith(
`${toFullFilePath(otherFileId)}.metadata`,
JSON.stringify({ ...metadata, fileSize: mockBuffer.length }),
{ encoding: 'utf-8' },
);
expect(result.fileSize).toBe(mockBuffer.length);
});
});
Expand Down

0 comments on commit 6d42fad

Please sign in to comment.