From 4df815e6ddba8e74efaf9777d16985a481b04089 Mon Sep 17 00:00:00 2001 From: yehorkardash Date: Mon, 8 Dec 2025 10:54:11 +0100 Subject: [PATCH] fix(Read/Write Files from Disk Node): Throw error when file not found, normalize windows path (#22455) --- .../Files/ReadWriteFile/ReadWriteFile.node.ts | 2 +- .../ReadWriteFile/actions/read.operation.ts | 26 +++++----- .../Files/ReadWriteFile/helpers/utils.ts | 14 ++++++ .../Files/ReadWriteFile/test/utils.test.ts | 48 +++++++++++++++---- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts index 33bf8dca62f..3c160cd6db0 100644 --- a/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts @@ -15,7 +15,7 @@ export class ReadWriteFile implements INodeType { name: 'readWriteFile', icon: 'file:readWriteFile.svg', group: ['input'], - version: 1, + version: [1, 1.1], description: 'Read or write files from the computer that runs n8n', defaults: { name: 'Read/Write Files from Disk', diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts index 89396660e4f..783981399ae 100644 --- a/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts @@ -1,5 +1,5 @@ import glob from 'fast-glob'; -import { NodeApiError } from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import type { IExecuteFunctions, INodeExecutionData, @@ -9,7 +9,7 @@ import type { import { updateDisplayOptions } from '@utils/utilities'; -import { errorMapper, escapeSpecialCharacters } from '../helpers/utils'; +import { errorMapper, normalizeFileSelector } from '../helpers/utils'; export const properties: INodeProperties[] = [ { @@ -76,18 +76,15 @@ const displayOptions = { export const description = updateDisplayOptions(displayOptions, properties); export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const nodeVersion = this.getNode().typeVersion; const returnData: INodeExecutionData[] = []; let fileSelector; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { try { - fileSelector = String(this.getNodeParameter('fileSelector', itemIndex)); - - fileSelector = escapeSpecialCharacters(fileSelector); - - if (/^[a-zA-Z]:/.test(fileSelector)) { - fileSelector = fileSelector.replace(/\\\\/g, '/'); - } + fileSelector = normalizeFileSelector( + this.getNodeParameter('fileSelector', itemIndex) as string, + ); const options = this.getNodeParameter('options', itemIndex, {}); @@ -99,6 +96,13 @@ export async function execute(this: IExecuteFunctions, items: INodeExecutionData const files = await glob(fileSelector); + if (files.length === 0 && nodeVersion > 1) { + throw new NodeOperationError(this.getNode(), 'No file(s) found', { + itemIndex, + description: `No file matching the selector "${fileSelector}" found`, + }); + } + const newItems: INodeExecutionData[] = []; for (const filePath of files) { const stream = await this.helpers.createReadStream(filePath); @@ -134,14 +138,14 @@ export async function execute(this: IExecuteFunctions, items: INodeExecutionData } returnData.push(...newItems); } catch (error) { - const nodeOperatioinError = errorMapper.call(this, error, itemIndex, { + const nodeOperationError = errorMapper.call(this, error, itemIndex, { filePath: fileSelector, operation: 'read', }); if (this.continueOnFail()) { returnData.push({ json: { - error: nodeOperatioinError.message, + error: nodeOperationError.message, }, pairedItem: { item: itemIndex, diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts index 706b9802927..cfab860e4b8 100644 --- a/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts @@ -1,5 +1,6 @@ import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; +import path from 'node:path'; export function errorMapper( this: IExecuteFunctions, @@ -39,3 +40,16 @@ export function escapeSpecialCharacters(str: string) { return str; } + +export function normalizeFileSelector(fileSelectorRaw: string) { + let fileSelector = String(fileSelectorRaw); + + const isWindows = /^[a-zA-Z]:/.test(fileSelector); + if (isWindows) { + fileSelector = path.win32.normalize(fileSelector).replace(/\\/g, '/'); + } + + fileSelector = escapeSpecialCharacters(fileSelector); + + return fileSelector; +} diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/test/utils.test.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/test/utils.test.ts index fb9dece3b20..5360d79a15e 100644 --- a/packages/nodes-base/nodes/Files/ReadWriteFile/test/utils.test.ts +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/test/utils.test.ts @@ -1,15 +1,43 @@ -import { escapeSpecialCharacters } from '../helpers/utils'; +import { escapeSpecialCharacters, normalizeFileSelector } from '../helpers/utils'; -describe('Read/Write Files from Disk, escapeSpecialCharacters', () => { - it('should escape parentheses in a string', () => { - const input = '/home/michael/Desktop/test(1).txt'; - const expectedOutput = '/home/michael/Desktop/test\\(1\\).txt'; - expect(escapeSpecialCharacters(input)).toBe(expectedOutput); +describe('Read/Write Files from Disk', () => { + describe('escapeSpecialCharacters', () => { + it('should escape parentheses in a string', () => { + const input = '/home/michael/Desktop/test(1).txt'; + const expectedOutput = '/home/michael/Desktop/test\\(1\\).txt'; + expect(escapeSpecialCharacters(input)).toBe(expectedOutput); + }); + + it('should not modify strings that do not contain parentheses', () => { + const input = '/home/michael/Desktop/test.txt'; + const expectedOutput = '/home/michael/Desktop/test.txt'; + expect(escapeSpecialCharacters(input)).toBe(expectedOutput); + }); }); - it('should not modify strings that do not contain parentheses', () => { - const input = '/home/michael/Desktop/test.txt'; - const expectedOutput = '/home/michael/Desktop/test.txt'; - expect(escapeSpecialCharacters(input)).toBe(expectedOutput); + describe('normalizeFileSelector', () => { + it('should normalize UNIX file selector with parentheses', () => { + const input = '/home/michael/Desktop/test(1).txt'; + const expectedOutput = '/home/michael/Desktop/test\\(1\\).txt'; + expect(normalizeFileSelector(input)).toBe(expectedOutput); + }); + + it('should normalize Windows file selector with \\ and parentheses', () => { + const input = 'C:\\Users\\michael\\Desktop\\test(1).txt'; + const expectedOutput = 'C:/Users/michael/Desktop/test\\(1\\).txt'; + expect(normalizeFileSelector(input)).toBe(expectedOutput); + }); + + it('should normalize Windows file selector with \\\\', () => { + const input = 'C:\\\\Users\\\\michael\\\\Desktop\\\\test.txt'; + const expectedOutput = 'C:/Users/michael/Desktop/test.txt'; + expect(normalizeFileSelector(input)).toBe(expectedOutput); + }); + + it('should normalize Windows file selector with /', () => { + const input = 'C:/Users/michael/Desktop/test.txt'; + const expectedOutput = 'C:/Users/michael/Desktop/test.txt'; + expect(normalizeFileSelector(input)).toBe(expectedOutput); + }); }); });