mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
6d42fad31a
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.
193 lines
5.5 KiB
TypeScript
193 lines
5.5 KiB
TypeScript
import path from 'node:path';
|
|
import fs from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { FileSystemManager } from '@/BinaryData/FileSystem.manager';
|
|
import { isStream } from '@/ObjectStore/utils';
|
|
import { toFileId, toStream } from './utils';
|
|
|
|
jest.mock('fs');
|
|
jest.mock('fs/promises');
|
|
|
|
const storagePath = tmpdir();
|
|
|
|
const fsManager = new FileSystemManager(storagePath);
|
|
|
|
const toFullFilePath = (fileId: string) => path.join(storagePath, fileId);
|
|
|
|
const workflowId = 'ObogjVbqpNOQpiyV';
|
|
const executionId = '999';
|
|
const fileUuid = '71f6209b-5d48-41a2-a224-80d529d8bb32';
|
|
const fileId = toFileId(workflowId, executionId, fileUuid);
|
|
|
|
const otherWorkflowId = 'FHio8ftV6SrCAfPJ';
|
|
const otherExecutionId = '888';
|
|
const otherFileUuid = '71f6209b-5d48-41a2-a224-80d529d8bb33';
|
|
const otherFileId = toFileId(otherWorkflowId, otherExecutionId, otherFileUuid);
|
|
|
|
const mockBuffer = Buffer.from('Test data');
|
|
const mockStream = toStream(mockBuffer);
|
|
|
|
afterAll(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('store()', () => {
|
|
it('should store a buffer', async () => {
|
|
const metadata = { mimeType: 'text/plain' };
|
|
|
|
const result = await fsManager.store(workflowId, executionId, mockBuffer, metadata);
|
|
|
|
expect(result.fileSize).toBe(mockBuffer.length);
|
|
});
|
|
});
|
|
|
|
describe('getPath()', () => {
|
|
it('should return a path', async () => {
|
|
const filePath = fsManager.getPath(fileId);
|
|
|
|
expect(filePath).toBe(toFullFilePath(fileId));
|
|
});
|
|
});
|
|
|
|
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);
|
|
|
|
expect(Buffer.isBuffer(result)).toBe(true);
|
|
expect(fsp.readFile).toHaveBeenCalledWith(toFullFilePath(fileId));
|
|
});
|
|
});
|
|
|
|
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);
|
|
|
|
expect(isStream(stream)).toBe(true);
|
|
expect(fs.createReadStream).toHaveBeenCalledWith(toFullFilePath(fileId), {
|
|
highWaterMark: undefined,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getMetadata()', () => {
|
|
it('should return metadata', async () => {
|
|
const mimeType = 'text/plain';
|
|
const fileName = 'file.txt';
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(
|
|
JSON.stringify({
|
|
fileSize: 1,
|
|
mimeType,
|
|
fileName,
|
|
}),
|
|
);
|
|
|
|
const metadata = await fsManager.getMetadata(fileId);
|
|
|
|
expect(metadata).toEqual(expect.objectContaining({ fileSize: 1, mimeType, fileName }));
|
|
});
|
|
});
|
|
|
|
describe('copyByFileId()', () => {
|
|
it('should copy by file ID and return the file ID', async () => {
|
|
fsp.copyFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
// @ts-expect-error - private method
|
|
jest.spyOn(fsManager, 'toFileId').mockReturnValue(otherFileId);
|
|
|
|
const targetFileId = await fsManager.copyByFileId(workflowId, executionId, fileId);
|
|
|
|
const sourcePath = toFullFilePath(fileId);
|
|
const targetPath = toFullFilePath(targetFileId);
|
|
|
|
expect(fsp.copyFile).toHaveBeenCalledWith(sourcePath, targetPath);
|
|
});
|
|
});
|
|
|
|
describe('copyByFilePath()', () => {
|
|
test('should copy by file path and return the file ID and size', async () => {
|
|
const sourceFilePath = tmpdir();
|
|
const metadata = { mimeType: 'text/plain' };
|
|
|
|
// @ts-expect-error - private method
|
|
jest.spyOn(fsManager, 'toFileId').mockReturnValue(otherFileId);
|
|
|
|
// @ts-expect-error - private method
|
|
jest.spyOn(fsManager, 'getSize').mockReturnValue(mockBuffer.length);
|
|
|
|
const targetPath = toFullFilePath(otherFileId);
|
|
|
|
fsp.cp = jest.fn().mockResolvedValue(undefined);
|
|
fsp.writeFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await fsManager.copyByFilePath(
|
|
workflowId,
|
|
executionId,
|
|
sourceFilePath,
|
|
metadata,
|
|
);
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe('deleteMany()', () => {
|
|
it('should delete many files by workflow ID and execution ID', async () => {
|
|
const ids = [
|
|
{ workflowId, executionId },
|
|
{ workflowId: otherWorkflowId, executionId: otherExecutionId },
|
|
];
|
|
|
|
fsp.rm = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const promise = fsManager.deleteMany(ids);
|
|
|
|
await expect(promise).resolves.not.toThrow();
|
|
|
|
expect(fsp.rm).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should suppress error on non-existing filepath', async () => {
|
|
const ids = [{ workflowId: 'does-not-exist', executionId: 'does-not-exist' }];
|
|
|
|
fsp.rm = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const promise = fsManager.deleteMany(ids);
|
|
|
|
await expect(promise).resolves.not.toThrow();
|
|
|
|
expect(fsp.rm).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('rename()', () => {
|
|
it('should rename a file', async () => {
|
|
fsp.rename = jest.fn().mockResolvedValue(undefined);
|
|
fsp.rm = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const promise = fsManager.rename(fileId, otherFileId);
|
|
|
|
const oldPath = toFullFilePath(fileId);
|
|
const newPath = toFullFilePath(otherFileId);
|
|
|
|
await expect(promise).resolves.not.toThrow();
|
|
|
|
expect(fsp.rename).toHaveBeenCalledTimes(2);
|
|
expect(fsp.rename).toHaveBeenCalledWith(oldPath, newPath);
|
|
expect(fsp.rename).toHaveBeenCalledWith(`${oldPath}.metadata`, `${newPath}.metadata`);
|
|
});
|
|
});
|