@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 +21 -0
- package/README.md +162 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/nodes/ExtractArchive/ExtractArchive.node.d.ts +12 -0
- package/dist/nodes/ExtractArchive/ExtractArchive.node.js +472 -0
- package/dist/nodes/ExtractArchive/extractarchive.svg +26 -0
- package/package.json +53 -0
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.
|
package/dist/index.d.ts
ADDED
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
|
+
}
|