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
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:
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.compression",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "The Compression node is useful when you want to compress files to either gzip or zip format. You can even use this node to decompress your gzip and zip files.",
|
||||
"categories": ["Core Nodes", "Data & Storage"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.compression/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": [
|
||||
"Zip",
|
||||
"Gzip",
|
||||
"uncompress",
|
||||
"compress",
|
||||
"decompress",
|
||||
"archive",
|
||||
"unarchive",
|
||||
"Binary",
|
||||
"Files",
|
||||
"File"
|
||||
],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Files", "Data Transformation"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
import * as fflate from 'fflate';
|
||||
import * as mime from 'mime-types';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
NodeOperationError,
|
||||
type IBinaryKeyData,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const gunzip = promisify(fflate.gunzip);
|
||||
const gzip = promisify(fflate.gzip);
|
||||
const unzip = promisify(fflate.unzip);
|
||||
const zip = promisify(fflate.zip);
|
||||
|
||||
const ALREADY_COMPRESSED = [
|
||||
'7z',
|
||||
'aifc',
|
||||
'bz2',
|
||||
'doc',
|
||||
'docx',
|
||||
'gif',
|
||||
'gz',
|
||||
'heic',
|
||||
'heif',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'mov',
|
||||
'mp3',
|
||||
'mp4',
|
||||
'pdf',
|
||||
'png',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'rar',
|
||||
'webm',
|
||||
'webp',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'zip',
|
||||
];
|
||||
|
||||
export class Compression implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Compression',
|
||||
name: 'compression',
|
||||
icon: 'fa:file-archive',
|
||||
iconColor: 'green',
|
||||
group: ['transform'],
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
version: [1, 1.1],
|
||||
description: 'Compress and decompress files',
|
||||
defaults: {
|
||||
name: 'Compression',
|
||||
color: '#408000',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Compress',
|
||||
value: 'compress',
|
||||
action: 'Compress file(s)',
|
||||
description: 'Compress files into a zip or gzip archive',
|
||||
},
|
||||
{
|
||||
name: 'Decompress',
|
||||
value: 'decompress',
|
||||
action: 'Decompress file(s)',
|
||||
description: 'Decompress zip or gzip archives',
|
||||
},
|
||||
],
|
||||
default: 'decompress',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Binary Field(s)',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['compress'],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. data,data2,data3',
|
||||
hint: 'The name of the input binary field(s) containing the file(s) to be compressed',
|
||||
description:
|
||||
'To process more than one file, use a comma-separated list of the binary fields names',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Binary Field(s)',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['decompress'],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input binary field(s) containing the file(s) to decompress',
|
||||
description:
|
||||
'To process more than one file, use a comma-separated list of the binary fields names',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Format',
|
||||
name: 'outputFormat',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
name: 'Gzip',
|
||||
value: 'gzip',
|
||||
},
|
||||
{
|
||||
name: 'Zip',
|
||||
value: 'zip',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['compress'],
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
description: 'Format of the output',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Format',
|
||||
name: 'outputFormat',
|
||||
type: 'options',
|
||||
default: 'zip',
|
||||
options: [
|
||||
{
|
||||
name: 'Gzip',
|
||||
value: 'gzip',
|
||||
},
|
||||
{
|
||||
name: 'Zip',
|
||||
value: 'zip',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['compress'],
|
||||
},
|
||||
hide: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
description: 'Format of the output',
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. data.zip',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['compress'],
|
||||
outputFormat: ['zip'],
|
||||
},
|
||||
},
|
||||
description: 'Name of the output file',
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output File in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
outputFormat: ['zip'],
|
||||
operation: ['compress'],
|
||||
},
|
||||
},
|
||||
hint: 'The name of the output binary field to put the file in',
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. data.txt',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['compress'],
|
||||
outputFormat: ['gzip'],
|
||||
},
|
||||
hide: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
description: 'Name of the output file',
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output File in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
outputFormat: ['gzip'],
|
||||
operation: ['compress'],
|
||||
},
|
||||
hide: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
hint: 'The name of the output binary field to put the file in',
|
||||
},
|
||||
{
|
||||
displayName: 'Output File Prefix',
|
||||
name: 'outputPrefix',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['compress'],
|
||||
outputFormat: ['gzip'],
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
description: 'Prefix to add to the gzip file',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Prefix',
|
||||
name: 'outputPrefix',
|
||||
type: 'string',
|
||||
default: 'file_',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['decompress'],
|
||||
},
|
||||
},
|
||||
description: 'Prefix to add to the decompressed files',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const length = items.length;
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (operation === 'decompress') {
|
||||
const binaryPropertyNames = this.getNodeParameter('binaryPropertyName', 0)
|
||||
.split(',')
|
||||
.map((key) => key.trim());
|
||||
|
||||
const outputPrefix = this.getNodeParameter('outputPrefix', 0) as string;
|
||||
|
||||
const binaryObject: IBinaryKeyData = {};
|
||||
|
||||
let zipIndex = 0;
|
||||
|
||||
for (const [index, binaryPropertyName] of binaryPropertyNames.entries()) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
const fileExtension = binaryData.fileExtension?.toLowerCase();
|
||||
|
||||
if (!fileExtension) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`File extension not found for binary data ${binaryPropertyName}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (fileExtension === 'zip') {
|
||||
const files = await unzip(binaryDataBuffer);
|
||||
|
||||
for (const key of Object.keys(files)) {
|
||||
// when files are compressed using MACOSX for some reason they are duplicated under __MACOSX
|
||||
if (key.includes('__MACOSX')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await this.helpers.prepareBinaryData(
|
||||
Buffer.from(files[key].buffer),
|
||||
key,
|
||||
);
|
||||
|
||||
binaryObject[`${outputPrefix}${zipIndex++}`] = data;
|
||||
}
|
||||
} else if (['gz', 'gzip'].includes(fileExtension)) {
|
||||
const file = await gunzip(binaryDataBuffer);
|
||||
|
||||
const fileName = binaryData.fileName?.split('.')[0];
|
||||
let fileExtension;
|
||||
let mimeType;
|
||||
|
||||
if (binaryData.fileName?.endsWith('.gz')) {
|
||||
const extractedFileExtension = binaryData.fileName.replace('.gz', '').split('.');
|
||||
if (extractedFileExtension.length > 1) {
|
||||
fileExtension = extractedFileExtension[extractedFileExtension.length - 1];
|
||||
mimeType = mime.lookup(fileExtension) as string;
|
||||
}
|
||||
}
|
||||
|
||||
const propertyName = `${outputPrefix}${index}`;
|
||||
|
||||
binaryObject[propertyName] = await this.helpers.prepareBinaryData(
|
||||
Buffer.from(file.buffer),
|
||||
fileName,
|
||||
mimeType,
|
||||
);
|
||||
|
||||
if (!fileExtension) {
|
||||
mimeType = binaryObject[propertyName].mimeType;
|
||||
fileExtension = mime.extension(mimeType) as string;
|
||||
}
|
||||
|
||||
binaryObject[propertyName].fileName = `${fileName}.${fileExtension}`;
|
||||
binaryObject[propertyName].fileExtension = fileExtension;
|
||||
binaryObject[propertyName].mimeType = mimeType as string;
|
||||
}
|
||||
}
|
||||
|
||||
returnData.push({
|
||||
json: items[i].json,
|
||||
binary: binaryObject,
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (operation === 'compress') {
|
||||
let binaryPropertyNameIndex = 0;
|
||||
if (nodeVersion > 1) {
|
||||
binaryPropertyNameIndex = i;
|
||||
}
|
||||
|
||||
const binaryPropertyNames = this.getNodeParameter(
|
||||
'binaryPropertyName',
|
||||
binaryPropertyNameIndex,
|
||||
)
|
||||
.split(',')
|
||||
.map((key) => key.trim());
|
||||
|
||||
const outputFormat = this.getNodeParameter('outputFormat', 0) as string;
|
||||
|
||||
const zipData: fflate.Zippable = {};
|
||||
const binaryObject: IBinaryKeyData = {};
|
||||
|
||||
for (const [index, binaryPropertyName] of binaryPropertyNames.entries()) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
|
||||
if (outputFormat === 'zip') {
|
||||
zipData[binaryData.fileName as string] = [
|
||||
binaryDataBuffer,
|
||||
{
|
||||
level: ALREADY_COMPRESSED.includes(binaryData.fileExtension as string) ? 0 : 6,
|
||||
},
|
||||
];
|
||||
} else if (outputFormat === 'gzip') {
|
||||
let outputPrefix;
|
||||
let fileName;
|
||||
let binaryProperty;
|
||||
let filePath;
|
||||
|
||||
if (nodeVersion > 1) {
|
||||
outputPrefix = this.getNodeParameter('binaryPropertyOutput', i, 'data');
|
||||
binaryProperty = `${outputPrefix}${index ? index : ''}`;
|
||||
|
||||
fileName = this.getNodeParameter('fileName', i, '') as string;
|
||||
if (!fileName) {
|
||||
fileName = binaryData.fileName?.split('.')[0];
|
||||
} else {
|
||||
fileName = fileName.replace('.gz', '').replace('.gzip', '');
|
||||
}
|
||||
|
||||
const fileExtension = binaryData.fileExtension
|
||||
? `.${binaryData.fileExtension.toLowerCase()}`
|
||||
: '';
|
||||
filePath = `${fileName}${fileExtension}.gz`;
|
||||
} else {
|
||||
outputPrefix = this.getNodeParameter('outputPrefix', 0) as string;
|
||||
binaryProperty = `${outputPrefix}${index}`;
|
||||
fileName = binaryData.fileName?.split('.')[0];
|
||||
filePath = `${fileName}.gzip`;
|
||||
}
|
||||
|
||||
const data = await gzip(binaryDataBuffer);
|
||||
|
||||
binaryObject[binaryProperty] = await this.helpers.prepareBinaryData(
|
||||
Buffer.from(data),
|
||||
filePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormat === 'zip') {
|
||||
let zipOptionsIndex = 0;
|
||||
if (nodeVersion > 1) {
|
||||
zipOptionsIndex = i;
|
||||
}
|
||||
const fileName = this.getNodeParameter('fileName', zipOptionsIndex) as string;
|
||||
const binaryPropertyOutput = this.getNodeParameter(
|
||||
'binaryPropertyOutput',
|
||||
zipOptionsIndex,
|
||||
);
|
||||
const buffer = await zip(zipData);
|
||||
const data = await this.helpers.prepareBinaryData(Buffer.from(buffer), fileName);
|
||||
|
||||
returnData.push({
|
||||
json: items[i].json,
|
||||
binary: {
|
||||
[binaryPropertyOutput]: data,
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (outputFormat === 'gzip') {
|
||||
returnData.push({
|
||||
json: items[i].json,
|
||||
binary: binaryObject,
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({
|
||||
json: {
|
||||
error: error.message,
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import type { WorkflowTestData } from 'n8n-workflow';
|
||||
import os from 'node:os';
|
||||
import path from 'path';
|
||||
|
||||
if (os.platform() !== 'win32') {
|
||||
describe('Execute Compression Node', () => {
|
||||
const testHarness = new NodeTestHarness();
|
||||
const workflowData = testHarness.readWorkflowJSON('workflow.compression.json');
|
||||
|
||||
const node = workflowData.nodes.find((n) => n.name === 'Read Binary File')!;
|
||||
node.parameters.filePath = path.join(__dirname, 'lorem.txt');
|
||||
|
||||
const tests: WorkflowTestData[] = [
|
||||
{
|
||||
description: 'nodes/Compression/test/node/workflow.compression.json',
|
||||
input: {
|
||||
workflowData,
|
||||
},
|
||||
output: {
|
||||
assertBinaryData: true,
|
||||
nodeData: {
|
||||
Compression1: [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
file_0: {
|
||||
mimeType: 'text/plain',
|
||||
fileType: 'text',
|
||||
fileExtension: 'txt',
|
||||
data: 'TG9yZW0gSXBzdW0KIk5lcXVlIHBvcnJvIHF1aXNxdWFtIGVzdCBxdWkgZG9sb3JlbSBpcHN1bSBxdWlhIGRvbG9yIHNpdCBhbWV0LCBjb25zZWN0ZXR1ciwgYWRpcGlzY2kgdmVsaXQuLi4iCiJUaGVyZSBpcyBubyBvbmUgd2hvIGxvdmVzIHBhaW4gaXRzZWxmLCB3aG8gc2Vla3MgYWZ0ZXIgaXQgYW5kIHdhbnRzIHRvIGhhdmUgaXQsIHNpbXBseSBiZWNhdXNlIGl0IGlzIHBhaW4uLi4iCkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBhbWV0LCBjb25zZWN0ZXR1ciBhZGlwaXNjaW5nIGVsaXQuIFZpdmFtdXMgZWZmaWNpdHVyIGF1Z3VlIGVnZXQgbGVvIGF1Y3RvciBpbnRlcmR1bS4gTWFlY2VuYXMgcGhhcmV0cmEgcGVsbGVudGVzcXVlIGp1c3RvIGF0IHBvcnR0aXRvci4gUGhhc2VsbHVzIHNvZGFsZXMgYWNjdW1zYW4gcG9zdWVyZS4gU2VkIHVybmEgYXVndWUsIG1hbGVzdWFkYSBldCBwbGFjZXJhdCBlZ2V0LCBhbGlxdWV0IHNlZCB0b3J0b3IuIFByYWVzZW50IGNvbW1vZG8sIGVyb3MgdmVsIGZlcm1lbnR1bSBhbGlxdWV0LCBkaWFtIGZlbGlzIHRlbXB1cyB1cm5hLCBuZWMgcG9zdWVyZSBsaWd1bGEgZWxpdCBxdWlzIGRpYW0uIFNlZCBub24gbG9yZW0gdm9sdXRwYXQsIG1vbGVzdGllIG5pYmggZWdldCwgcHVsdmluYXIgZWxpdC4gTWFlY2VuYXMgYWMgdmFyaXVzIGV4LCBxdWlzIHVsdHJpY2VzIHNhcGllbi4gVml2YW11cyBlZ2VzdGFzLCByaXN1cyBzaXQgYW1ldCBtYWxlc3VhZGEgZWxlaWZlbmQsIG1hc3NhIHRvcnRvciBlbGVtZW50dW0gdHVycGlzLCBpZCBzb2xsaWNpdHVkaW4gZXN0IGxpYmVybyBzZWQgZHVpLiBOdWxsYSBpZCBwdWx2aW5hciBzZW0sIG5lYyBoZW5kcmVyaXQgbGFjdXMuCgpWZXN0aWJ1bHVtIGFsaXF1YW0gZXQgcHVydXMgc2l0IGFtZXQgdWxsYW1jb3JwZXIuIE5hbSB2aXZlcnJhIHRyaXN0aXF1ZSBmaW5pYnVzLiBWaXZhbXVzIGRpZ25pc3NpbSB0aW5jaWR1bnQgZXN0LiBEb25lYyBqdXN0byB0dXJwaXMsIGZldWdpYXQgc29sbGljaXR1ZGluIHN1c2NpcGl0IGEsIGJsYW5kaXQgY3Vyc3VzIHF1YW0uIER1aXMgdml2ZXJyYSBzZW0gdXQgbGFjaW5pYSBzZW1wZXIuIEFlbmVhbiB1bHRyaWNlcyBhbnRlIGV0IG5pc2kgc2VtcGVyIGRhcGlidXMuIE1vcmJpIHF1aXMgZnJpbmdpbGxhIG1hZ25hLiBOdW5jIGxpZ3VsYSBhcmN1LCBhbGlxdWFtIHF1aXMgbGVvIGV0LCBjb21tb2RvIG1vbGVzdGllIGxlY3R1cy4gUHJvaW4gYWMgbGVvIGZlcm1lbnR1bSwgY29udmFsbGlzIGVyYXQgaW4sIHVsdHJpY2llcyBtYWduYS4gTW9yYmkgbG9yZW0gYXJjdSwgY29uZ3VlIHNlZCBkaWFtIGlkLCBtb2xlc3RpZSBhbGlxdWV0IGVyYXQuIEFsaXF1YW0gdXQgZHVpIHBvc3VlcmUsIHB1bHZpbmFyIHNhcGllbiBzZWQsIGludGVyZHVtIHNhcGllbi4gRG9uZWMgc29sbGljaXR1ZGluIGxpYmVybyBpbXBlcmRpZXQgYWxpcXVhbSBhbGlxdWFtLgoKRG9uZWMgbm9uIG9kaW8gbWFzc2EuIE1hZWNlbmFzIGV1IGludGVyZHVtIGlwc3VtLCBhdCBncmF2aWRhIHR1cnBpcy4gVmVzdGlidWx1bSBldSBtb2xsaXMgdHVycGlzLCBlZ2V0IGZhdWNpYnVzIGxpZ3VsYS4gVmVzdGlidWx1bSBhbnRlIGlwc3VtIHByaW1pcyBpbiBmYXVjaWJ1cyBvcmNpIGx1Y3R1cyBldCB1bHRyaWNlcyBwb3N1ZXJlIGN1YmlsaWEgY3VyYWU7IEFlbmVhbiB0cmlzdGlxdWUgZXggbmVjIHJpc3VzIGJpYmVuZHVtLCBldSB1bGxhbWNvcnBlciBlcm9zIHBoYXJldHJhLiBVdCBmaW5pYnVzIGp1c3RvIGxlY3R1cywgbm9uIHZhcml1cyBhbnRlIHZvbHV0cGF0IGV1LiBQZWxsZW50ZXNxdWUgdHVycGlzIGVyb3MsIG1vbGVzdGllIGVnZXQgZW5pbSBuZWMsIGVsZW1lbnR1bSBwb3N1ZXJlIHJpc3VzLiBQaGFzZWxsdXMgZGljdHVtLCBlc3Qgdml0YWUgdml2ZXJyYSBlZ2VzdGFzLCBlbGl0IGVyb3MgcG9zdWVyZSBsb3JlbSwgaWQgZmluaWJ1cyBzYXBpZW4gbnVsbGEgc2l0IGFtZXQgYW50ZS4gRXRpYW0gaW4gcHVydXMgaWQgdGVsbHVzIHBvcnR0aXRvciBwb3N1ZXJlLgoKRG9uZWMgbm9uIGx1Y3R1cyBlbmltLiBWaXZhbXVzIHNvbGxpY2l0dWRpbiB0dXJwaXMgcXVpcyBxdWFtIGZlcm1lbnR1bSBwb3J0dGl0b3IuIFByb2luIGlkIHRpbmNpZHVudCBlcmF0LiBBZW5lYW4gaGVuZHJlcml0IHNpdCBhbWV0IGFyY3UgdmVuZW5hdGlzIHZ1bHB1dGF0ZS4gUGVsbGVudGVzcXVlIG5vbiBlcm9zIHZvbHV0cGF0LCB2ZXN0aWJ1bHVtIG1pIG5vbiwgdm9sdXRwYXQgYXJjdS4gSW4gYWMgdWxsYW1jb3JwZXIgc2FwaWVuLiBVdCBzaXQgYW1ldCB1cm5hIGFjIG51bmMgYWNjdW1zYW4gYXVjdG9yLiBGdXNjZSBhY2N1bXNhbiBsaWJlcm8gdmVsIHByZXRpdW0gaWFjdWxpcy4gUHJvaW4gc2VkIHZlaGljdWxhIGVyYXQuIEludGVnZXIgZmF1Y2lidXMgYXVndWUgbnVuYywgbmVjIGJsYW5kaXQgYXJjdSBsdWN0dXMgZGlnbmlzc2ltLiBNYWVjZW5hcyBhbGlxdWFtIHNvbGxpY2l0dWRpbiBkdWksIGF0IHBsYWNlcmF0IGVyYXQgbWFsZXN1YWRhIHV0LiBTdXNwZW5kaXNzZSBub24gcnV0cnVtIG9kaW8sIHV0IHVsdHJpY2llcyBtYXNzYS4KCk1vcmJpIHNpdCBhbWV0IHJpc3VzIHZlc3RpYnVsdW0sIHZlaGljdWxhIGVsaXQgdmVsLCB1bGxhbWNvcnBlciBhbnRlLiBBZW5lYW4gcnV0cnVtIHBlbGxlbnRlc3F1ZSBmZWxpcywgbmVjIHBoYXJldHJhIGlwc3VtIGNvbnNlY3RldHVyIHF1aXMuIE1hdXJpcyBpbiBlbGl0IGV1IG51bGxhIGRhcGlidXMgZmV1Z2lhdC4gSW4gdmVzdGlidWx1bSBtYXNzYSBpZCBsYW9yZWV0IHNvZGFsZXMuIFN1c3BlbmRpc3NlIG9ybmFyZSBjb25ndWUgbWV0dXMsIGluIHVsdHJpY2llcyBuZXF1ZSBjb25zZXF1YXQgbmVjLiBJbnRlcmR1bSBldCBtYWxlc3VhZGEgZmFtZXMgYWMgYW50ZSBpcHN1bSBwcmltaXMgaW4gZmF1Y2lidXMuIE1hZWNlbmFzIHNlZCBsdWN0dXMgdmVsaXQuIFF1aXNxdWUgaW4gbnVuYyBwZWxsZW50ZXNxdWUsIGFsaXF1ZXQgcXVhbSBpbiwgYWNjdW1zYW4gbGVjdHVzLiBWZXN0aWJ1bHVtIGRhcGlidXMgdG9ydG9yIGFjIG1ldHVzIGZldWdpYXQgY3Vyc3VzLiBTZWQgbmVjIHNjZWxlcmlzcXVlIGFyY3UuCg==',
|
||||
fileName: 'lorem.txt',
|
||||
fileSize: '3.07 kB',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testData of tests) {
|
||||
testHarness.setupTest(testData);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
describe('Execute Compression Node', () => {
|
||||
it('Skipped because compression results are different on platform win32', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode, IBinaryData } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import * as fflate from 'fflate';
|
||||
|
||||
import { Compression } from '../../Compression.node';
|
||||
|
||||
jest.mock('fflate');
|
||||
|
||||
const mockFflate = (
|
||||
method: 'unzip' | 'gunzip' | 'zip' | 'gunzip',
|
||||
data: any,
|
||||
error: any = null,
|
||||
) => {
|
||||
jest.mocked(fflate[method]).mockImplementation((_, callback) => {
|
||||
callback(error, data);
|
||||
return () => {};
|
||||
});
|
||||
};
|
||||
|
||||
describe('Compression Node - Decompress Operation', () => {
|
||||
let compression: Compression;
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
const mockNode: INode = {
|
||||
id: 'test-node',
|
||||
name: 'Compression',
|
||||
type: 'n8n-nodes-base.compression',
|
||||
typeVersion: 1.1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
compression = new Compression();
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { test: 'data' } }]);
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
operation: 'decompress',
|
||||
binaryPropertyName: 'data',
|
||||
outputPrefix: 'file_',
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Zip Decompression', () => {
|
||||
it('should decompress a zip file successfully', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
const mockZipContents = {
|
||||
file1_txt: new Uint8Array([72, 101, 108, 108, 111]),
|
||||
file2_txt: new Uint8Array([87, 111, 114, 108, 100]),
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock zip data'));
|
||||
mockFflate('unzip', mockZipContents);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockImplementation(
|
||||
async (buffer, fileName) =>
|
||||
({
|
||||
data: buffer.toString('base64'),
|
||||
mimeType: 'text/plain',
|
||||
fileName: fileName ?? 'file',
|
||||
fileExtension: 'txt',
|
||||
}) as IBinaryData,
|
||||
);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0].json).toEqual({ test: 'data' });
|
||||
expect(result[0][0].binary).toBeDefined();
|
||||
expect(result[0][0].binary?.file_0).toBeDefined();
|
||||
expect(result[0][0].binary?.file_1).toBeDefined();
|
||||
expect(result[0][0].pairedItem).toEqual({ item: 0 });
|
||||
expect(fflate.unzip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should skip __MACOSX files when decompressing zip', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
const mockZipContents = {
|
||||
file1_txt: new Uint8Array([72, 101, 108, 108, 111]),
|
||||
__MACOSX_file1_txt: new Uint8Array([0, 0, 0]),
|
||||
file2_txt: new Uint8Array([87, 111, 114, 108, 100]),
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock zip data'));
|
||||
mockFflate('unzip', mockZipContents);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockImplementation(
|
||||
async (buffer, fileName) =>
|
||||
({
|
||||
data: buffer.toString('base64'),
|
||||
mimeType: 'text/plain',
|
||||
fileName: fileName ?? 'file',
|
||||
fileExtension: 'txt',
|
||||
}) as IBinaryData,
|
||||
);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].binary?.file_0).toBeDefined();
|
||||
expect(result[0][0].binary?.file_1).toBeDefined();
|
||||
expect(jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData)).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should process multiple zip files from comma-separated binary properties', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
operation: 'decompress',
|
||||
binaryPropertyName: 'data1,data2',
|
||||
outputPrefix: 'file_',
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
const mockZipContents = {
|
||||
file_txt: new Uint8Array([72, 101, 108, 108, 111]),
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock zip data'));
|
||||
mockFflate('unzip', mockZipContents);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockImplementation(
|
||||
async (buffer, fileName) =>
|
||||
({
|
||||
data: buffer.toString('base64'),
|
||||
mimeType: 'text/plain',
|
||||
fileName: fileName ?? 'file',
|
||||
fileExtension: 'txt',
|
||||
}) as IBinaryData,
|
||||
);
|
||||
|
||||
await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(fflate.unzip).toHaveBeenCalledTimes(2);
|
||||
expect(jest.mocked(mockExecuteFunctions.helpers.assertBinaryData)).toHaveBeenCalledWith(
|
||||
0,
|
||||
'data1',
|
||||
);
|
||||
expect(jest.mocked(mockExecuteFunctions.helpers.assertBinaryData)).toHaveBeenCalledWith(
|
||||
0,
|
||||
'data2',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gzip Decompression', () => {
|
||||
it('should decompress a gzip file successfully', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'test.txt.gz',
|
||||
fileExtension: 'gz',
|
||||
};
|
||||
|
||||
const mockGunzipData = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock gzip data'));
|
||||
mockFflate('gunzip', mockGunzipData);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockResolvedValue({
|
||||
data: 'SGVsbG8=',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'test.txt',
|
||||
fileExtension: 'txt',
|
||||
} as IBinaryData);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0].binary?.file_0).toBeDefined();
|
||||
expect(result[0][0].binary?.file_0?.fileName).toBe('test.txt');
|
||||
expect(result[0][0].binary?.file_0?.fileExtension).toBe('txt');
|
||||
expect(result[0][0].binary?.file_0?.mimeType).toBe('text/plain');
|
||||
expect(fflate.gunzip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle gzip file with .gzip extension', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'test.txt.gzip',
|
||||
fileExtension: 'gzip',
|
||||
};
|
||||
|
||||
const mockGunzipData = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock gzip data'));
|
||||
mockFflate('gunzip', mockGunzipData);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockResolvedValue({
|
||||
data: 'SGVsbG8=',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'test.txt',
|
||||
fileExtension: 'txt',
|
||||
} as IBinaryData);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].binary?.file_0).toBeDefined();
|
||||
expect(fflate.gunzip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should determine mime type and file extension for gzip file', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'data.json.gz',
|
||||
fileExtension: 'gz',
|
||||
};
|
||||
|
||||
const mockGunzipData = new Uint8Array([123, 125]);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock gzip data'));
|
||||
mockFflate('gunzip', mockGunzipData);
|
||||
|
||||
let callCount = 0;
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.prepareBinaryData)
|
||||
.mockImplementation(async (buffer, fileName, mimeType) => {
|
||||
callCount++;
|
||||
if (callCount === 1) {
|
||||
return {
|
||||
data: buffer.toString('base64'),
|
||||
mimeType: 'application/json',
|
||||
fileName: fileName ?? 'data',
|
||||
fileExtension: 'json',
|
||||
} as IBinaryData;
|
||||
}
|
||||
return {
|
||||
data: buffer.toString('base64'),
|
||||
mimeType: mimeType ?? 'application/json',
|
||||
fileName: fileName ?? 'data',
|
||||
fileExtension: 'json',
|
||||
} as IBinaryData;
|
||||
});
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].binary?.file_0?.fileName).toBe('data.json');
|
||||
expect(result[0][0].binary?.file_0?.fileExtension).toBe('json');
|
||||
expect(result[0][0].binary?.file_0?.mimeType).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should process multiple gzip files', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
operation: 'decompress',
|
||||
binaryPropertyName: 'data1,data2',
|
||||
outputPrefix: 'file_',
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'test.txt.gz',
|
||||
fileExtension: 'gz',
|
||||
};
|
||||
|
||||
const mockGunzipData = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock gzip data'));
|
||||
mockFflate('gunzip', mockGunzipData);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockResolvedValue({
|
||||
data: 'SGVsbG8=',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'test.txt',
|
||||
fileExtension: 'txt',
|
||||
} as IBinaryData);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].binary?.file_0).toBeDefined();
|
||||
expect(result[0][0].binary?.file_1).toBeDefined();
|
||||
expect(fflate.gunzip).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should throw error when file extension is not found', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/octet-stream',
|
||||
fileName: undefined,
|
||||
fileExtension: undefined,
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
|
||||
await expect(compression.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle decompression errors with continueOnFail', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('invalid zip data'));
|
||||
mockFflate('unzip', null, new Error('Invalid zip file'));
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0].json).toHaveProperty('error', 'Invalid zip file');
|
||||
expect(result[0][0].pairedItem).toEqual({ item: 0 });
|
||||
});
|
||||
|
||||
it('should throw error when continueOnFail is false', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('invalid zip data'));
|
||||
mockFflate('unzip', null, new Error('Invalid zip file'));
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
await expect(compression.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'Invalid zip file',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle gunzip errors with continueOnFail', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'test.txt.gz',
|
||||
fileExtension: 'gz',
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('invalid gzip data'));
|
||||
mockFflate('gunzip', null, new Error('Invalid gzip file'));
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].json).toHaveProperty('error', 'Invalid gzip file');
|
||||
});
|
||||
|
||||
it('should throw when fileExtension is missing', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock zip data'));
|
||||
|
||||
await expect(compression.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Items Processing', () => {
|
||||
it('should process multiple input items', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([
|
||||
{ json: { item: 1 } },
|
||||
{ json: { item: 2 } },
|
||||
]);
|
||||
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'test.txt.gz',
|
||||
fileExtension: 'gz',
|
||||
};
|
||||
|
||||
const mockGunzipData = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock gzip data'));
|
||||
mockFflate('gunzip', mockGunzipData);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockResolvedValue({
|
||||
data: 'SGVsbG8=',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'test.txt',
|
||||
fileExtension: 'txt',
|
||||
} as IBinaryData);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0]).toHaveLength(2);
|
||||
expect(result[0][0].json).toEqual({ item: 1 });
|
||||
expect(result[0][1].json).toEqual({ item: 2 });
|
||||
expect(result[0][0].pairedItem).toEqual({ item: 0 });
|
||||
expect(result[0][1].pairedItem).toEqual({ item: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should use custom output prefix', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
operation: 'decompress',
|
||||
binaryPropertyName: 'data',
|
||||
outputPrefix: 'extracted_',
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'test.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
const mockZipContents = {
|
||||
file1_txt: new Uint8Array([72, 101, 108, 108, 111]),
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock zip data'));
|
||||
mockFflate('unzip', mockZipContents);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockResolvedValue({
|
||||
data: 'SGVsbG8=',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'file1.txt',
|
||||
fileExtension: 'txt',
|
||||
} as IBinaryData);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].binary?.extracted_0).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle empty zip archive', async () => {
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/zip',
|
||||
fileName: 'empty.zip',
|
||||
fileExtension: 'zip',
|
||||
};
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock zip data'));
|
||||
mockFflate('unzip', {});
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result[0][0].binary).toEqual({});
|
||||
expect(result[0][0].json).toEqual({ test: 'data' });
|
||||
});
|
||||
|
||||
it('should trim whitespace from binary property names', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
operation: 'decompress',
|
||||
binaryPropertyName: ' data1 , data2 ',
|
||||
outputPrefix: 'file_',
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
|
||||
const mockBinaryData: IBinaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/gzip',
|
||||
fileName: 'test.txt.gz',
|
||||
fileExtension: 'gz',
|
||||
};
|
||||
|
||||
const mockGunzipData = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.assertBinaryData).mockReturnValue(mockBinaryData);
|
||||
jest
|
||||
.mocked(mockExecuteFunctions.helpers.getBinaryDataBuffer)
|
||||
.mockResolvedValue(Buffer.from('mock gzip data'));
|
||||
mockFflate('gunzip', mockGunzipData);
|
||||
|
||||
jest.mocked(mockExecuteFunctions.helpers.prepareBinaryData).mockResolvedValue({
|
||||
data: 'SGVsbG8=',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'test.txt',
|
||||
fileExtension: 'txt',
|
||||
} as IBinaryData);
|
||||
|
||||
const result = await compression.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(jest.mocked(mockExecuteFunctions.helpers.assertBinaryData)).toHaveBeenCalledWith(
|
||||
0,
|
||||
'data1',
|
||||
);
|
||||
expect(jest.mocked(mockExecuteFunctions.helpers.assertBinaryData)).toHaveBeenCalledWith(
|
||||
0,
|
||||
'data2',
|
||||
);
|
||||
expect(result[0][0].binary?.file_0).toBeDefined();
|
||||
expect(result[0][0].binary?.file_1).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
Lorem Ipsum
|
||||
"Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
|
||||
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus efficitur augue eget leo auctor interdum. Maecenas pharetra pellentesque justo at porttitor. Phasellus sodales accumsan posuere. Sed urna augue, malesuada et placerat eget, aliquet sed tortor. Praesent commodo, eros vel fermentum aliquet, diam felis tempus urna, nec posuere ligula elit quis diam. Sed non lorem volutpat, molestie nibh eget, pulvinar elit. Maecenas ac varius ex, quis ultrices sapien. Vivamus egestas, risus sit amet malesuada eleifend, massa tortor elementum turpis, id sollicitudin est libero sed dui. Nulla id pulvinar sem, nec hendrerit lacus.
|
||||
|
||||
Vestibulum aliquam et purus sit amet ullamcorper. Nam viverra tristique finibus. Vivamus dignissim tincidunt est. Donec justo turpis, feugiat sollicitudin suscipit a, blandit cursus quam. Duis viverra sem ut lacinia semper. Aenean ultrices ante et nisi semper dapibus. Morbi quis fringilla magna. Nunc ligula arcu, aliquam quis leo et, commodo molestie lectus. Proin ac leo fermentum, convallis erat in, ultricies magna. Morbi lorem arcu, congue sed diam id, molestie aliquet erat. Aliquam ut dui posuere, pulvinar sapien sed, interdum sapien. Donec sollicitudin libero imperdiet aliquam aliquam.
|
||||
|
||||
Donec non odio massa. Maecenas eu interdum ipsum, at gravida turpis. Vestibulum eu mollis turpis, eget faucibus ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Aenean tristique ex nec risus bibendum, eu ullamcorper eros pharetra. Ut finibus justo lectus, non varius ante volutpat eu. Pellentesque turpis eros, molestie eget enim nec, elementum posuere risus. Phasellus dictum, est vitae viverra egestas, elit eros posuere lorem, id finibus sapien nulla sit amet ante. Etiam in purus id tellus porttitor posuere.
|
||||
|
||||
Donec non luctus enim. Vivamus sollicitudin turpis quis quam fermentum porttitor. Proin id tincidunt erat. Aenean hendrerit sit amet arcu venenatis vulputate. Pellentesque non eros volutpat, vestibulum mi non, volutpat arcu. In ac ullamcorper sapien. Ut sit amet urna ac nunc accumsan auctor. Fusce accumsan libero vel pretium iaculis. Proin sed vehicula erat. Integer faucibus augue nunc, nec blandit arcu luctus dignissim. Maecenas aliquam sollicitudin dui, at placerat erat malesuada ut. Suspendisse non rutrum odio, ut ultricies massa.
|
||||
|
||||
Morbi sit amet risus vestibulum, vehicula elit vel, ullamcorper ante. Aenean rutrum pellentesque felis, nec pharetra ipsum consectetur quis. Mauris in elit eu nulla dapibus feugiat. In vestibulum massa id laoreet sodales. Suspendisse ornare congue metus, in ultricies neque consequat nec. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas sed luctus velit. Quisque in nunc pellentesque, aliquet quam in, accumsan lectus. Vestibulum dapibus tortor ac metus feugiat cursus. Sed nec scelerisque arcu.
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "Compression test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "abf9ed91-0b05-49df-82d3-1bafb6e6f698",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "compress",
|
||||
"outputFormat": "zip",
|
||||
"fileName": "data.zip"
|
||||
},
|
||||
"id": "a84b995a-bd44-42a0-b5e0-a8ff4e879814",
|
||||
"name": "Compression",
|
||||
"type": "n8n-nodes-base.compression",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"filePath": "/home/me/Desktop/lorem.txt"
|
||||
},
|
||||
"id": "2fdbf11d-e1ae-4091-b15f-e0eed69e87c3",
|
||||
"name": "Read Binary File",
|
||||
"type": "n8n-nodes-base.readBinaryFile",
|
||||
"typeVersion": 1,
|
||||
"position": [600, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "959fa107-f3e6-444e-8fd4-1c893b8942db",
|
||||
"name": "Compression1",
|
||||
"type": "n8n-nodes-base.compression",
|
||||
"typeVersion": 1,
|
||||
"position": [1000, 400]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read Binary File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read Binary File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compression",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Compression": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compression1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "9868d72c-88bc-4bcc-a85b-586ae1b6b023",
|
||||
"id": "112",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
Reference in New Issue
Block a user