@beltar/n8n-nodes-extract-archive 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Beltar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # n8n-nodes-extract-archive
2
+
3
+ Extract ZIP and RAR archive files in n8n.
4
+
5
+ ## Features
6
+
7
+ - **ZIP and RAR support** with auto-detection
8
+ - **Binary data or file path input** - use files from previous nodes or server paths
9
+ - **Password-protected archives** support
10
+ - **File filtering** - extract only specific file types
11
+ - **Subdirectory handling** - include or exclude nested folders
12
+ - **Flexible output** - single item with all files or one item per file
13
+
14
+ ## Requirements
15
+
16
+ This node requires extraction tools to be installed on your system.
17
+
18
+ ### Docker (n8n image)
19
+
20
+ ```dockerfile
21
+ FROM n8nio/n8n:latest
22
+
23
+ USER root
24
+
25
+ # For ZIP support
26
+ RUN apk add --no-cache unzip
27
+
28
+ # For RAR support (option 1: p7zip)
29
+ RUN apk add --no-cache p7zip
30
+
31
+ # For RAR support (option 2: unrar from EDM115)
32
+ RUN apk add --no-cache curl jq libstdc++ libgcc && \
33
+ curl -LsSf https://api.github.com/repos/EDM115/unrar-alpine/releases/latest \
34
+ | jq -r '.assets[] | select(.name == "unrar") | .id' \
35
+ | xargs -I {} curl -LsSf https://api.github.com/repos/EDM115/unrar-alpine/releases/assets/{} \
36
+ | jq -r '.browser_download_url' \
37
+ | xargs -I {} curl -Lsf {} -o /tmp/unrar && \
38
+ install -v -m755 /tmp/unrar /usr/local/bin && \
39
+ rm /tmp/unrar
40
+
41
+ USER node
42
+ ```
43
+
44
+ ### Complete Docker example with this node
45
+
46
+ ```dockerfile
47
+ FROM n8nio/n8n:latest
48
+
49
+ USER root
50
+
51
+ RUN apk add --no-cache unzip p7zip poppler-utils
52
+
53
+ RUN mkdir -p /opt/n8n-nodes && chown node:node /opt/n8n-nodes
54
+
55
+ USER node
56
+
57
+ RUN cd /opt/n8n-nodes && \
58
+ npm init -y && \
59
+ npm install @beltar/n8n-nodes-extract-archive @beltar/n8n-nodes-pdf-to-image
60
+
61
+ ENV N8N_CUSTOM_EXTENSIONS=/opt/n8n-nodes/node_modules
62
+ ```
63
+
64
+ ### Debian/Ubuntu
65
+
66
+ ```bash
67
+ sudo apt-get install unzip unrar
68
+ ```
69
+
70
+ ### Alpine Linux
71
+
72
+ ```bash
73
+ apk add unzip p7zip
74
+ ```
75
+
76
+ ### macOS
77
+
78
+ ```bash
79
+ brew install unzip unrar
80
+ ```
81
+
82
+ ## Installation
83
+
84
+ ### In n8n
85
+
86
+ 1. Go to **Settings** → **Community Nodes**
87
+ 2. Enter `@beltar/n8n-nodes-extract-archive`
88
+ 3. Click **Install**
89
+ 4. Restart n8n
90
+
91
+ ### Manual installation
92
+
93
+ ```bash
94
+ cd ~/.n8n
95
+ npm install @beltar/n8n-nodes-extract-archive
96
+ ```
97
+
98
+ ## Usage
99
+
100
+ 1. Add the **Extract Archive** node to your workflow
101
+ 2. Connect a node that provides an archive file (binary data or file path)
102
+ 3. Configure the options:
103
+ - **Input Mode**: Binary Data or File Path
104
+ - **Input Binary Field**: Name of the binary property (for binary mode)
105
+ - **File Path**: Path to archive on server (for file path mode)
106
+ - **Archive Type**: Auto-detect, ZIP, or RAR
107
+ - **Password**: For encrypted archives (optional)
108
+ - **Output Mode**: Single item with all files, or one item per file
109
+ - **Output Binary Field**: Name for the output binary property
110
+ - **Include Subdirectories**: Whether to include nested files
111
+ - **File Filter**: Comma-separated patterns (e.g., `*.txt, *.pdf`)
112
+
113
+ ## Output
114
+
115
+ ### Single Item Mode
116
+
117
+ Returns one item with:
118
+ - `json.totalFiles`: Number of files extracted
119
+ - `json.archiveType`: Type of archive (zip/rar)
120
+ - `json.files`: Array of file info objects
121
+ - `json.binaryProperties`: Array of binary property names
122
+ - `binary.file_1`, `binary.file_2`, etc.: The extracted files
123
+
124
+ ### Multiple Items Mode
125
+
126
+ Returns one item per file with:
127
+ - `json.index`: File index
128
+ - `json.totalFiles`: Total number of files
129
+ - `json.fileName`: Name of the file
130
+ - `json.filePath`: Relative path within archive
131
+ - `json.fileSize`: Size in bytes
132
+ - `json.archiveType`: Type of archive
133
+ - `binary.file`: The extracted file
134
+
135
+ ## Example Workflows
136
+
137
+ ### Extract and process each file
138
+ ```
139
+ [HTTP Request: Download ZIP] → [Extract Archive] → [For Each File: Process]
140
+ ```
141
+
142
+ ### Extract specific file types
143
+ ```
144
+ [Read Binary File] → [Extract Archive (filter: *.pdf)] → [PDF to Image]
145
+ ```
146
+
147
+ ### Extract from server path
148
+ ```
149
+ [Set file path] → [Extract Archive (file path mode)] → [Upload to S3]
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT
155
+
156
+ ## Author
157
+
158
+ Beltar
159
+
160
+ ## Contributing
161
+
162
+ Contributions are welcome! Please open an issue or submit a pull request.
@@ -0,0 +1,2 @@
1
+ import { ExtractArchive } from './nodes/ExtractArchive/ExtractArchive.node';
2
+ export { ExtractArchive };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExtractArchive = void 0;
4
+ const ExtractArchive_node_1 = require("./nodes/ExtractArchive/ExtractArchive.node");
5
+ Object.defineProperty(exports, "ExtractArchive", { enumerable: true, get: function () { return ExtractArchive_node_1.ExtractArchive; } });
@@ -0,0 +1,12 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class ExtractArchive implements INodeType {
3
+ description: INodeTypeDescription;
4
+ private detectArchiveType;
5
+ private checkToolAvailable;
6
+ private extractZip;
7
+ private extractRar;
8
+ private getAllFiles;
9
+ private matchesFilter;
10
+ private getMimeType;
11
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
12
+ }
@@ -0,0 +1,472 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ExtractArchive = void 0;
37
+ const n8n_workflow_1 = require("n8n-workflow");
38
+ const child_process_1 = require("child_process");
39
+ const fs = __importStar(require("fs"));
40
+ const crypto = __importStar(require("crypto"));
41
+ const path = __importStar(require("path"));
42
+ const util_1 = require("util");
43
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
44
+ class ExtractArchive {
45
+ constructor() {
46
+ this.description = {
47
+ displayName: 'Extract Archive',
48
+ name: 'extractArchive',
49
+ icon: 'file:extractarchive.svg',
50
+ group: ['transform'],
51
+ version: 1,
52
+ subtitle: '={{$parameter["inputMode"]}} - {{$parameter["archiveType"]}}',
53
+ description: 'Extract ZIP and RAR archive files',
54
+ defaults: {
55
+ name: 'Extract Archive',
56
+ },
57
+ inputs: ['main'],
58
+ outputs: ['main'],
59
+ properties: [
60
+ {
61
+ displayName: 'Input Mode',
62
+ name: 'inputMode',
63
+ type: 'options',
64
+ options: [
65
+ { name: 'Binary Data', value: 'binary', description: 'Use binary data from previous node' },
66
+ { name: 'File Path', value: 'filePath', description: 'Use a file path on the server' },
67
+ ],
68
+ default: 'binary',
69
+ description: 'How to get the archive file',
70
+ },
71
+ {
72
+ displayName: 'Input Binary Field',
73
+ name: 'binaryPropertyName',
74
+ type: 'string',
75
+ default: 'data',
76
+ required: true,
77
+ description: 'Name of the binary property containing the archive file',
78
+ displayOptions: {
79
+ show: {
80
+ inputMode: ['binary'],
81
+ },
82
+ },
83
+ },
84
+ {
85
+ displayName: 'File Path',
86
+ name: 'filePath',
87
+ type: 'string',
88
+ default: '',
89
+ required: true,
90
+ placeholder: '/path/to/archive.zip',
91
+ description: 'Path to the archive file on the server',
92
+ displayOptions: {
93
+ show: {
94
+ inputMode: ['filePath'],
95
+ },
96
+ },
97
+ },
98
+ {
99
+ displayName: 'Archive Type',
100
+ name: 'archiveType',
101
+ type: 'options',
102
+ options: [
103
+ { name: 'Auto-Detect', value: 'auto', description: 'Detect type from file extension or content' },
104
+ { name: 'ZIP', value: 'zip' },
105
+ { name: 'RAR', value: 'rar' },
106
+ ],
107
+ default: 'auto',
108
+ description: 'The type of archive to extract',
109
+ },
110
+ {
111
+ displayName: 'Password',
112
+ name: 'password',
113
+ type: 'string',
114
+ typeOptions: {
115
+ password: true,
116
+ },
117
+ default: '',
118
+ description: 'Password for encrypted archives (optional)',
119
+ },
120
+ {
121
+ displayName: 'Output Mode',
122
+ name: 'outputMode',
123
+ type: 'options',
124
+ options: [
125
+ {
126
+ name: 'Single Item (All Files)',
127
+ value: 'single',
128
+ description: 'Return one item with all extracted files as separate binary properties'
129
+ },
130
+ {
131
+ name: 'Multiple Items (One Per File)',
132
+ value: 'multiple',
133
+ description: 'Return separate items for each extracted file'
134
+ },
135
+ ],
136
+ default: 'multiple',
137
+ description: 'How to output the extracted files',
138
+ },
139
+ {
140
+ displayName: 'Output Binary Field',
141
+ name: 'outputBinaryPropertyName',
142
+ type: 'string',
143
+ default: 'file',
144
+ required: true,
145
+ description: 'Name of the binary property for output files. For single mode, files are named file_1, file_2, etc.',
146
+ },
147
+ {
148
+ displayName: 'Include Subdirectories',
149
+ name: 'includeSubdirectories',
150
+ type: 'boolean',
151
+ default: true,
152
+ description: 'Whether to include files from subdirectories in the output',
153
+ },
154
+ {
155
+ displayName: 'File Filter',
156
+ name: 'fileFilter',
157
+ type: 'string',
158
+ default: '',
159
+ placeholder: '*.txt, *.pdf',
160
+ description: 'Comma-separated list of file patterns to extract (e.g., "*.txt, *.pdf"). Leave empty to extract all files.',
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ detectArchiveType(filePath, mimeType) {
166
+ const ext = path.extname(filePath).toLowerCase();
167
+ // Check by extension first
168
+ if (ext === '.zip' || ext === '.zipx')
169
+ return 'zip';
170
+ if (ext === '.rar')
171
+ return 'rar';
172
+ // Check by MIME type
173
+ if (mimeType) {
174
+ if (mimeType.includes('zip'))
175
+ return 'zip';
176
+ if (mimeType.includes('rar'))
177
+ return 'rar';
178
+ }
179
+ // Try to detect by file content (magic bytes)
180
+ try {
181
+ const buffer = Buffer.alloc(8);
182
+ const fd = fs.openSync(filePath, 'r');
183
+ fs.readSync(fd, buffer, 0, 8, 0);
184
+ fs.closeSync(fd);
185
+ // ZIP magic bytes: PK (0x50 0x4B)
186
+ if (buffer[0] === 0x50 && buffer[1] === 0x4B)
187
+ return 'zip';
188
+ // RAR magic bytes: Rar! (0x52 0x61 0x72 0x21)
189
+ if (buffer[0] === 0x52 && buffer[1] === 0x61 && buffer[2] === 0x72 && buffer[3] === 0x21)
190
+ return 'rar';
191
+ }
192
+ catch {
193
+ // Ignore detection errors
194
+ }
195
+ // Default to zip
196
+ return 'zip';
197
+ }
198
+ checkToolAvailable(tool) {
199
+ try {
200
+ (0, child_process_1.execSync)(`which ${tool}`, { encoding: 'utf-8', stdio: 'pipe' });
201
+ return true;
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }
207
+ async extractZip(archivePath, outputDir, password) {
208
+ if (!this.checkToolAvailable('unzip')) {
209
+ throw new Error('unzip is not installed. Please install it: apk add unzip (Alpine) or apt-get install unzip (Debian/Ubuntu)');
210
+ }
211
+ const args = ['-o', '-q']; // Overwrite, quiet
212
+ if (password) {
213
+ args.push('-P', password);
214
+ }
215
+ args.push(archivePath, '-d', outputDir);
216
+ const command = `unzip ${args.map(a => `"${a}"`).join(' ')}`;
217
+ await execAsync(command);
218
+ }
219
+ async extractRar(archivePath, outputDir, password) {
220
+ // Try unrar first, then 7z as fallback
221
+ const hasUnrar = this.checkToolAvailable('unrar');
222
+ const has7z = this.checkToolAvailable('7z');
223
+ if (!hasUnrar && !has7z) {
224
+ throw new Error('Neither unrar nor 7z is installed. Please install unrar or p7zip: apk add p7zip (Alpine) or apt-get install unrar (Debian/Ubuntu)');
225
+ }
226
+ if (hasUnrar) {
227
+ const args = ['x', '-o+', '-y']; // Extract with path, overwrite, assume yes
228
+ if (password) {
229
+ args.push(`-p${password}`);
230
+ }
231
+ else {
232
+ args.push('-p-'); // No password
233
+ }
234
+ args.push(archivePath, outputDir + '/');
235
+ const command = `unrar ${args.map(a => `"${a}"`).join(' ')}`;
236
+ await execAsync(command);
237
+ }
238
+ else {
239
+ // Use 7z
240
+ const args = ['x', '-y', `-o${outputDir}`];
241
+ if (password) {
242
+ args.push(`-p${password}`);
243
+ }
244
+ args.push(archivePath);
245
+ const command = `7z ${args.map(a => `"${a}"`).join(' ')}`;
246
+ await execAsync(command);
247
+ }
248
+ }
249
+ getAllFiles(dir, includeSubdirs) {
250
+ const files = [];
251
+ const readDir = (currentDir, relativePath = '') => {
252
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
253
+ for (const entry of entries) {
254
+ const fullPath = path.join(currentDir, entry.name);
255
+ const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
256
+ if (entry.isDirectory()) {
257
+ if (includeSubdirs) {
258
+ readDir(fullPath, relPath);
259
+ }
260
+ }
261
+ else if (entry.isFile()) {
262
+ files.push(relPath);
263
+ }
264
+ }
265
+ };
266
+ readDir(dir);
267
+ return files;
268
+ }
269
+ matchesFilter(fileName, filter) {
270
+ if (!filter.trim())
271
+ return true;
272
+ const patterns = filter.split(',').map(p => p.trim().toLowerCase());
273
+ const lowerFileName = fileName.toLowerCase();
274
+ for (const pattern of patterns) {
275
+ if (pattern.startsWith('*.')) {
276
+ // Extension match
277
+ const ext = pattern.slice(1); // Remove *
278
+ if (lowerFileName.endsWith(ext))
279
+ return true;
280
+ }
281
+ else if (pattern.includes('*')) {
282
+ // Simple wildcard match
283
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
284
+ if (regex.test(lowerFileName))
285
+ return true;
286
+ }
287
+ else {
288
+ // Exact match
289
+ if (lowerFileName === pattern)
290
+ return true;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+ getMimeType(fileName) {
296
+ const ext = path.extname(fileName).toLowerCase();
297
+ const mimeTypes = {
298
+ '.txt': 'text/plain',
299
+ '.html': 'text/html',
300
+ '.htm': 'text/html',
301
+ '.css': 'text/css',
302
+ '.js': 'application/javascript',
303
+ '.json': 'application/json',
304
+ '.xml': 'application/xml',
305
+ '.pdf': 'application/pdf',
306
+ '.doc': 'application/msword',
307
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
308
+ '.xls': 'application/vnd.ms-excel',
309
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
310
+ '.ppt': 'application/vnd.ms-powerpoint',
311
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
312
+ '.png': 'image/png',
313
+ '.jpg': 'image/jpeg',
314
+ '.jpeg': 'image/jpeg',
315
+ '.gif': 'image/gif',
316
+ '.svg': 'image/svg+xml',
317
+ '.webp': 'image/webp',
318
+ '.ico': 'image/x-icon',
319
+ '.mp3': 'audio/mpeg',
320
+ '.wav': 'audio/wav',
321
+ '.mp4': 'video/mp4',
322
+ '.webm': 'video/webm',
323
+ '.zip': 'application/zip',
324
+ '.rar': 'application/vnd.rar',
325
+ '.7z': 'application/x-7z-compressed',
326
+ '.tar': 'application/x-tar',
327
+ '.gz': 'application/gzip',
328
+ };
329
+ return mimeTypes[ext] || 'application/octet-stream';
330
+ }
331
+ async execute() {
332
+ const items = this.getInputData();
333
+ const returnData = [];
334
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
335
+ try {
336
+ const inputMode = this.getNodeParameter('inputMode', itemIndex);
337
+ const archiveTypeParam = this.getNodeParameter('archiveType', itemIndex);
338
+ const password = this.getNodeParameter('password', itemIndex);
339
+ const outputMode = this.getNodeParameter('outputMode', itemIndex);
340
+ const outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', itemIndex);
341
+ const includeSubdirectories = this.getNodeParameter('includeSubdirectories', itemIndex);
342
+ const fileFilter = this.getNodeParameter('fileFilter', itemIndex);
343
+ const uniqueId = crypto.randomUUID();
344
+ const tmpDir = '/tmp';
345
+ const extractDir = path.join(tmpDir, `extract-${uniqueId}`);
346
+ let archivePath = '';
347
+ let shouldDeleteArchive = false;
348
+ let mimeType;
349
+ try {
350
+ // Create extraction directory
351
+ fs.mkdirSync(extractDir, { recursive: true });
352
+ if (inputMode === 'binary') {
353
+ const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex);
354
+ if (!items[itemIndex].binary?.[binaryPropertyName]) {
355
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No binary data found in property "${binaryPropertyName}"`, { itemIndex });
356
+ }
357
+ const binaryData = items[itemIndex].binary[binaryPropertyName];
358
+ mimeType = binaryData.mimeType;
359
+ const ext = binaryData.fileExtension || path.extname(binaryData.fileName || '');
360
+ archivePath = path.join(tmpDir, `archive-${uniqueId}${ext}`);
361
+ const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
362
+ fs.writeFileSync(archivePath, buffer);
363
+ shouldDeleteArchive = true;
364
+ }
365
+ else {
366
+ archivePath = this.getNodeParameter('filePath', itemIndex);
367
+ if (!fs.existsSync(archivePath)) {
368
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `File not found: ${archivePath}`, { itemIndex });
369
+ }
370
+ }
371
+ // Detect or use specified archive type
372
+ const extractor = new ExtractArchive();
373
+ const archiveType = archiveTypeParam === 'auto'
374
+ ? extractor.detectArchiveType(archivePath, mimeType)
375
+ : archiveTypeParam;
376
+ // Extract the archive
377
+ if (archiveType === 'zip') {
378
+ await extractor.extractZip(archivePath, extractDir, password || undefined);
379
+ }
380
+ else if (archiveType === 'rar') {
381
+ await extractor.extractRar(archivePath, extractDir, password || undefined);
382
+ }
383
+ // Get all extracted files
384
+ let files = extractor.getAllFiles(extractDir, includeSubdirectories);
385
+ // Apply file filter
386
+ if (fileFilter.trim()) {
387
+ files = files.filter(f => extractor.matchesFilter(path.basename(f), fileFilter));
388
+ }
389
+ if (files.length === 0) {
390
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No files were extracted. The archive might be empty or all files were filtered out.', { itemIndex });
391
+ }
392
+ if (outputMode === 'single') {
393
+ const binary = {};
394
+ const fileInfos = [];
395
+ for (let i = 0; i < files.length; i++) {
396
+ const relPath = files[i];
397
+ const fullPath = path.join(extractDir, relPath);
398
+ const fileBuffer = fs.readFileSync(fullPath);
399
+ const fileName = path.basename(relPath);
400
+ const propertyName = files.length === 1
401
+ ? outputBinaryPropertyName
402
+ : `${outputBinaryPropertyName}_${i + 1}`;
403
+ binary[propertyName] = await this.helpers.prepareBinaryData(fileBuffer, fileName, extractor.getMimeType(fileName));
404
+ fileInfos.push({
405
+ name: fileName,
406
+ path: relPath,
407
+ size: fileBuffer.length,
408
+ });
409
+ }
410
+ returnData.push({
411
+ json: {
412
+ totalFiles: files.length,
413
+ archiveType,
414
+ files: fileInfos,
415
+ binaryProperties: files.map((_, i) => files.length === 1 ? outputBinaryPropertyName : `${outputBinaryPropertyName}_${i + 1}`),
416
+ },
417
+ binary,
418
+ });
419
+ }
420
+ else {
421
+ for (let i = 0; i < files.length; i++) {
422
+ const relPath = files[i];
423
+ const fullPath = path.join(extractDir, relPath);
424
+ const fileBuffer = fs.readFileSync(fullPath);
425
+ const fileName = path.basename(relPath);
426
+ returnData.push({
427
+ json: {
428
+ index: i + 1,
429
+ totalFiles: files.length,
430
+ fileName,
431
+ filePath: relPath,
432
+ fileSize: fileBuffer.length,
433
+ archiveType,
434
+ },
435
+ binary: {
436
+ [outputBinaryPropertyName]: await this.helpers.prepareBinaryData(fileBuffer, fileName, extractor.getMimeType(fileName)),
437
+ },
438
+ });
439
+ }
440
+ }
441
+ }
442
+ finally {
443
+ // Cleanup
444
+ try {
445
+ if (shouldDeleteArchive && archivePath && fs.existsSync(archivePath)) {
446
+ fs.unlinkSync(archivePath);
447
+ }
448
+ if (fs.existsSync(extractDir)) {
449
+ fs.rmSync(extractDir, { recursive: true, force: true });
450
+ }
451
+ }
452
+ catch {
453
+ // Ignore cleanup errors
454
+ }
455
+ }
456
+ }
457
+ catch (error) {
458
+ if (this.continueOnFail()) {
459
+ returnData.push({
460
+ json: {
461
+ error: error.message,
462
+ },
463
+ });
464
+ continue;
465
+ }
466
+ throw error;
467
+ }
468
+ }
469
+ return [returnData];
470
+ }
471
+ }
472
+ exports.ExtractArchive = ExtractArchive;
@@ -0,0 +1,26 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <!-- Archive/ZIP file -->
3
+ <rect x="4" y="8" width="28" height="36" rx="2" fill="#FF9800" stroke="#E65100" stroke-width="2"/>
4
+ <!-- Zipper pattern -->
5
+ <rect x="14" y="8" width="8" height="4" fill="#424242"/>
6
+ <rect x="14" y="14" width="8" height="4" fill="#424242"/>
7
+ <rect x="14" y="20" width="8" height="4" fill="#424242"/>
8
+ <rect x="14" y="26" width="8" height="4" fill="#424242"/>
9
+ <rect x="14" y="32" width="8" height="4" fill="#424242"/>
10
+ <rect x="14" y="38" width="8" height="4" fill="#E65100"/>
11
+
12
+ <!-- Arrow -->
13
+ <path d="M36 26 L44 26 L44 22 L52 28 L44 34 L44 30 L36 30 Z" fill="#4CAF50"/>
14
+
15
+ <!-- Extracted files -->
16
+ <rect x="40" y="12" width="20" height="14" rx="1" fill="#2196F3" stroke="#1565C0" stroke-width="1.5"/>
17
+ <line x1="44" y1="17" x2="56" y2="17" stroke="white" stroke-width="1.5"/>
18
+ <line x1="44" y1="21" x2="52" y2="21" stroke="white" stroke-width="1.5"/>
19
+
20
+ <rect x="44" y="32" width="18" height="14" rx="1" fill="#9C27B0" stroke="#6A1B9A" stroke-width="1.5"/>
21
+ <line x1="48" y1="37" x2="58" y2="37" stroke="white" stroke-width="1.5"/>
22
+ <line x1="48" y1="41" x2="54" y2="41" stroke="white" stroke-width="1.5"/>
23
+
24
+ <rect x="38" y="50" width="22" height="12" rx="1" fill="#4CAF50" stroke="#2E7D32" stroke-width="1.5"/>
25
+ <line x1="42" y1="55" x2="56" y2="55" stroke="white" stroke-width="1.5"/>
26
+ </svg>
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@beltar/n8n-nodes-extract-archive",
3
+ "version": "1.0.1",
4
+ "description": "n8n node to extract ZIP and RAR archive files. Supports binary data or file path input.",
5
+ "keywords": [
6
+ "n8n",
7
+ "n8n-community-node-package",
8
+ "archive",
9
+ "extract",
10
+ "zip",
11
+ "rar",
12
+ "unzip",
13
+ "unrar",
14
+ "decompress"
15
+ ],
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Beltar"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/beltar/n8n-nodes-extract-archive.git"
23
+ },
24
+ "main": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "scripts": {
27
+ "build": "tsc && npm run copy-assets",
28
+ "copy-assets": "cp src/nodes/ExtractArchive/*.svg dist/nodes/ExtractArchive/ 2>/dev/null || true",
29
+ "dev": "tsc --watch",
30
+ "lint": "eslint . --ext .ts",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "n8n": {
37
+ "n8nNodesApiVersion": 1,
38
+ "nodes": [
39
+ "dist/nodes/ExtractArchive/ExtractArchive.node.js"
40
+ ]
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.10.0",
44
+ "n8n-workflow": "^1.20.0",
45
+ "typescript": "^5.3.0"
46
+ },
47
+ "peerDependencies": {
48
+ "n8n-workflow": "*"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ }
53
+ }