first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,163 @@
import glob from 'fast-glob';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import type {
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
JsonObject,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { errorMapper, normalizeFileSelector } from '../helpers/utils';
export const properties: INodeProperties[] = [
{
displayName: 'File(s) Selector',
name: 'fileSelector',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. /home/user/Pictures/**/*.png',
hint: 'Supports patterns, learn more <a href="https://github.com/micromatch/picomatch#basic-globbing" target="_blank">here</a>',
description:
"Specify a file's path or path pattern to read multiple files. Always use forward-slashes for path separator even on Windows.",
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'File Extension',
name: 'fileExtension',
type: 'string',
default: '',
placeholder: 'e.g. zip',
description: 'Extension of the file in the output binary',
},
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
placeholder: 'e.g. data.zip',
description: 'Name of the file in the output binary',
},
{
displayName: 'Mime Type',
name: 'mimeType',
type: 'string',
default: '',
placeholder: 'e.g. application/zip',
description: 'Mime type of the file in the output binary',
},
{
displayName: 'Put Output File in Field',
name: 'dataPropertyName',
type: 'string',
default: 'data',
placeholder: 'e.g. data',
description: "By default 'data' is used",
hint: 'The name of the output binary field to put the file in',
},
],
},
];
const displayOptions = {
show: {
operation: ['read'],
},
};
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 = normalizeFileSelector(
this.getNodeParameter('fileSelector', itemIndex) as string,
);
const options = this.getNodeParameter('options', itemIndex, {});
let dataPropertyName = 'data';
if (options.dataPropertyName) {
dataPropertyName = options.dataPropertyName as string;
}
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(
await this.helpers.resolvePath(filePath),
);
const binaryData = await this.helpers.prepareBinaryData(stream, filePath);
if (options.fileName !== undefined) {
binaryData.fileName = options.fileName as string;
}
if (options.fileExtension !== undefined) {
binaryData.fileExtension = options.fileExtension as string;
}
if (options.mimeType !== undefined) {
binaryData.mimeType = options.mimeType as string;
}
newItems.push({
binary: {
[dataPropertyName]: binaryData,
},
json: {
mimeType: binaryData.mimeType,
fileType: binaryData.fileType,
fileName: binaryData.fileName,
fileExtension: binaryData.fileExtension,
fileSize: binaryData.fileSize,
},
pairedItem: {
item: itemIndex,
},
});
}
returnData.push(...newItems);
} catch (error) {
const nodeOperationError = errorMapper.call(this, error, itemIndex, {
filePath: fileSelector,
operation: 'read',
});
if (this.continueOnFail()) {
returnData.push({
json: {
error: nodeOperationError.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex });
}
}
return returnData;
}
@@ -0,0 +1,135 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
JsonObject,
} from 'n8n-workflow';
import { BINARY_ENCODING, NodeApiError } from 'n8n-workflow';
import type { Readable } from 'stream';
import { updateDisplayOptions } from '@utils/utilities';
import { errorMapper } from '../helpers/utils';
import { constants } from 'node:fs';
export const properties: INodeProperties[] = [
{
displayName: 'File Path and Name',
name: 'fileName',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. /data/example.jpg',
description:
'Path and name of the file that should be written. Also include the file extension.',
},
{
displayName: 'Input Binary Field',
name: 'dataPropertyName',
type: 'string',
default: 'data',
placeholder: 'e.g. data',
required: true,
hint: 'The name of the input binary field containing the file to be written',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Append',
name: 'append',
type: 'boolean',
default: false,
description:
"Whether to append to an existing file. While it's commonly used with text files, it's not limited to them, however, it wouldn't be applicable for file types that have a specific structure like most binary formats.",
},
],
},
];
const displayOptions = {
show: {
operation: ['write'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) {
const returnData: INodeExecutionData[] = [];
let fileName;
let item: INodeExecutionData;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex);
fileName = this.getNodeParameter('fileName', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {});
const flag: number = options.append
? constants.O_APPEND
: constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC;
item = items[itemIndex];
const newItem: INodeExecutionData = {
json: {},
pairedItem: {
item: itemIndex,
},
};
Object.assign(newItem.json, item.json);
const binaryData = this.helpers.assertBinaryData(itemIndex, dataPropertyName);
let fileContent: Buffer | Readable;
if (binaryData.id) {
fileContent = await this.helpers.getBinaryStream(binaryData.id);
} else {
fileContent = Buffer.from(binaryData.data, BINARY_ENCODING);
}
// Write the file to disk
await this.helpers.writeContentToFile(
await this.helpers.resolvePath(fileName),
fileContent,
flag,
);
if (item.binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
newItem.binary = {};
Object.assign(newItem.binary, item.binary);
}
// Add the file name to data
newItem.json.fileName = fileName;
returnData.push(newItem);
} catch (error) {
const nodeOperatioinError = errorMapper.call(this, error, itemIndex, {
filePath: fileName,
operation: 'write',
});
if (this.continueOnFail()) {
returnData.push({
json: {
error: nodeOperatioinError.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex });
}
}
return returnData;
}