n8n-nodes-binary-to-url 0.0.11 → 0.1.2
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/README.md +110 -333
- package/dist/drivers/MemoryStorage.d.ts +14 -10
- package/dist/drivers/MemoryStorage.js +113 -37
- package/dist/nodes/BinaryToUrl/BinaryToUrl.node.js +47 -15
- package/package.json +1 -1
- package/dist/drivers/index.d.ts +0 -1
- package/dist/drivers/index.js +0 -5
package/README.md
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Binary to URL - n8n Community Node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**Binary to URL - n8n Community Node**
|
|
6
|
-
|
|
7
|
-
Create temporary URLs for binary files within workflow execution
|
|
3
|
+
Create temporary URLs for binary files within n8n workflow execution.
|
|
8
4
|
|
|
9
5
|
[](https://www.npmjs.com/package/n8n-nodes-binary-to-url)
|
|
10
6
|
[](https://opensource.org/licenses/MIT)
|
|
11
7
|
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
8
|
---
|
|
15
9
|
|
|
16
|
-
##
|
|
10
|
+
## Important Notice
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
This node is designed for **temporary URL sharing within workflow execution**, NOT for long-term file storage.
|
|
19
13
|
|
|
20
|
-
- ❌
|
|
21
|
-
- ❌
|
|
22
|
-
- ✅
|
|
23
|
-
- ✅
|
|
24
|
-
- ✅
|
|
14
|
+
- ❌ NOT a file storage service
|
|
15
|
+
- ❌ NOT for long-term URL sharing
|
|
16
|
+
- ✅ FOR temporary URL passing between workflow nodes
|
|
17
|
+
- ✅ FOR short-term external access (minutes to hours)
|
|
18
|
+
- ✅ FOR workflow-internal binary data handling
|
|
25
19
|
|
|
26
20
|
Files are stored in memory and automatically deleted after expiration.
|
|
27
21
|
|
|
@@ -29,123 +23,64 @@ Files are stored in memory and automatically deleted after expiration.
|
|
|
29
23
|
|
|
30
24
|
## Features
|
|
31
25
|
|
|
32
|
-
- **In-Memory Storage** - Store files temporarily in n8n memory
|
|
33
|
-
- **Temporary URLs** - Create short-lived URLs for binary data
|
|
34
|
-
- **Zero Configuration** - No setup required
|
|
35
|
-
- **Automatic Cleanup** - Files expire automatically
|
|
36
|
-
- **Cache Management** - Built-in LRU cache with
|
|
37
|
-
- **Workflow
|
|
38
|
-
- **File Type Validation** - Security validation for allowed MIME types
|
|
39
|
-
- **Memory Efficient** - Automatic cleanup of expired and old files
|
|
40
|
-
|
|
41
|
-
## Table of Contents
|
|
42
|
-
|
|
43
|
-
- [Installation](#installation)
|
|
44
|
-
- [Quick Start](#quick-start)
|
|
45
|
-
- [Configuration](#configuration)
|
|
46
|
-
- [Usage Examples](#usage-examples)
|
|
47
|
-
- [Architecture](#architecture)
|
|
48
|
-
- [API Reference](#api-reference)
|
|
49
|
-
- [Security](#security)
|
|
50
|
-
- [Troubleshooting](#troubleshooting)
|
|
51
|
-
- [Development](#development)
|
|
26
|
+
- **In-Memory Storage** - Store files temporarily in n8n memory
|
|
27
|
+
- **Temporary URLs** - Create short-lived URLs for binary data
|
|
28
|
+
- **Zero Configuration** - No setup required
|
|
29
|
+
- **Automatic Cleanup** - Files expire automatically
|
|
30
|
+
- **Cache Management** - Built-in LRU cache with limits
|
|
31
|
+
- **Workflow Isolation** - Each workflow has isolated storage
|
|
52
32
|
|
|
53
33
|
---
|
|
54
34
|
|
|
55
35
|
## Installation
|
|
56
36
|
|
|
57
|
-
### Install via npm
|
|
58
|
-
|
|
59
37
|
```bash
|
|
60
38
|
npm install n8n-nodes-binary-to-url
|
|
61
39
|
```
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
1. Go to your n8n installation directory
|
|
66
|
-
2. Run the npm install command above
|
|
67
|
-
3. Restart n8n
|
|
68
|
-
4. The "Binary to URL" node will appear in the node palette
|
|
69
|
-
|
|
70
|
-
---
|
|
41
|
+
Then restart n8n:
|
|
71
42
|
|
|
72
|
-
|
|
43
|
+
```bash
|
|
44
|
+
# If using npm
|
|
45
|
+
n8n restart
|
|
73
46
|
|
|
74
|
-
|
|
47
|
+
# If using Docker
|
|
48
|
+
docker-compose restart n8n
|
|
75
49
|
|
|
76
|
-
|
|
50
|
+
# If using systemd
|
|
51
|
+
sudo systemctl restart n8n
|
|
52
|
+
```
|
|
77
53
|
|
|
78
|
-
|
|
54
|
+
---
|
|
79
55
|
|
|
80
|
-
|
|
81
|
-
Workflow:
|
|
82
|
-
1. HTTP Request Node (download image)
|
|
83
|
-
2. Binary to URL (create temporary URL)
|
|
84
|
-
3. HTTP Request Node (send URL to another API)
|
|
85
|
-
4. Binary to URL (delete file - optional)
|
|
86
|
-
```
|
|
56
|
+
## Quick Start
|
|
87
57
|
|
|
88
|
-
|
|
58
|
+
### Upload Operation
|
|
89
59
|
|
|
90
60
|
1. Add a **Binary to URL** node to your workflow
|
|
91
|
-
2. Select
|
|
92
|
-
3.
|
|
93
|
-
|
|
61
|
+
2. Select **Upload** operation
|
|
62
|
+
3. Configure:
|
|
63
|
+
- **Binary Property**: `data` (default)
|
|
64
|
+
- **URL Expiration Time**: `600` (10 minutes)
|
|
65
|
+
4. Connect to a node with binary data (e.g., HTTP Request)
|
|
94
66
|
5. Execute the workflow
|
|
95
67
|
|
|
96
68
|
**Output:**
|
|
97
69
|
|
|
98
70
|
```json
|
|
99
71
|
{
|
|
100
|
-
"fileKey": "
|
|
101
|
-
"proxyUrl": "https://your-n8n.com/webhook/123/file/
|
|
72
|
+
"fileKey": "1736567890123-abc123def456",
|
|
73
|
+
"proxyUrl": "https://your-n8n.com/webhook/123/file/1736567890123-abc123def456",
|
|
102
74
|
"contentType": "image/jpeg",
|
|
103
75
|
"fileSize": 245678
|
|
104
76
|
}
|
|
105
77
|
```
|
|
106
78
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- **Within workflow**: Pass `proxyUrl` to subsequent nodes that need file access
|
|
110
|
-
- **External access**: Open `proxyUrl` in browser or API calls (will expire after TTL)
|
|
79
|
+
### Delete Operation
|
|
111
80
|
|
|
112
|
-
**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
## Configuration
|
|
117
|
-
|
|
118
|
-
### Operations
|
|
119
|
-
|
|
120
|
-
#### Upload Operation
|
|
121
|
-
|
|
122
|
-
| Parameter | Type | Required | Default | Description |
|
|
123
|
-
| ----------------------- | ------ | -------- | ------- | --------------------------------------------------- |
|
|
124
|
-
| **Binary Property** | string | ❌ No | `data` | Name of binary property containing the file |
|
|
125
|
-
| **URL Expiration Time** | number | ❌ No | `600` | How long URL remains valid (default: 10 minutes) |
|
|
126
|
-
|
|
127
|
-
**Recommended TTL values:**
|
|
128
|
-
|
|
129
|
-
- **60-300 seconds** (1-5 minutes): For workflow-internal use
|
|
130
|
-
- **300-600 seconds** (5-10 minutes): For short-term processing
|
|
131
|
-
- **600-3600 seconds** (10-60 minutes): For longer operations (not recommended)
|
|
132
|
-
|
|
133
|
-
#### Delete Operation
|
|
134
|
-
|
|
135
|
-
| Parameter | Type | Required | Default | Description |
|
|
136
|
-
| ------------ | ------ | -------- | ------- | ------------------------- |
|
|
137
|
-
| **File Key** | string | ✅ Yes\* | - | Key of the file to delete |
|
|
138
|
-
|
|
139
|
-
\*Can also be provided from previous node via `fileKey` property
|
|
140
|
-
|
|
141
|
-
### Storage Limits
|
|
142
|
-
|
|
143
|
-
- **Maximum file size:** 100 MB
|
|
144
|
-
- **Maximum cache size:** 100 MB
|
|
145
|
-
- **Default TTL:** 600 seconds (10 minutes)
|
|
146
|
-
- **Minimum TTL:** 60 seconds (1 minute)
|
|
147
|
-
|
|
148
|
-
When cache is full, oldest files are automatically removed to make space for new uploads.
|
|
81
|
+
1. Add **Binary to URL** node
|
|
82
|
+
2. Select **Delete** operation
|
|
83
|
+
3. Enter **File Key** (or use from previous node's `fileKey`)
|
|
149
84
|
|
|
150
85
|
---
|
|
151
86
|
|
|
@@ -153,290 +88,132 @@ When cache is full, oldest files are automatically removed to make space for new
|
|
|
153
88
|
|
|
154
89
|
### Example 1: Pass Binary Data Between Nodes
|
|
155
90
|
|
|
156
|
-
**Scenario:** Download an image, process it with an external API, then delete.
|
|
157
|
-
|
|
158
|
-
```yaml
|
|
159
|
-
Workflow:
|
|
160
|
-
1. HTTP Request (download image from URL A)
|
|
161
|
-
2. Binary to URL (TTL: 300 = 5 minutes)
|
|
162
|
-
3. HTTP Request (send proxyUrl to external API for processing)
|
|
163
|
-
4. Binary to URL (operation: Delete, cleanup)
|
|
164
91
|
```
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
```yaml
|
|
171
|
-
Workflow:
|
|
172
|
-
1. Generate PDF report
|
|
173
|
-
2. Binary to URL (TTL: 600 = 10 minutes)
|
|
174
|
-
3. Send Email (attach using proxyUrl)
|
|
175
|
-
4. Binary to URL (operation: Delete, optional - will auto-expire)
|
|
92
|
+
1. HTTP Request (download image)
|
|
93
|
+
2. Binary to URL (Upload, TTL: 300)
|
|
94
|
+
3. HTTP Request (send proxyUrl to API)
|
|
95
|
+
4. Binary to URL (Delete)
|
|
176
96
|
```
|
|
177
97
|
|
|
178
|
-
|
|
98
|
+
### Example 2: Temporary Email Attachment
|
|
179
99
|
|
|
180
|
-
### Example 3: Batch Processing
|
|
181
|
-
|
|
182
|
-
**Scenario:** Process multiple files with an external service.
|
|
183
|
-
|
|
184
|
-
```yaml
|
|
185
|
-
Workflow:
|
|
186
|
-
1. Read Binary Files (from folder)
|
|
187
|
-
2. Split In Batches
|
|
188
|
-
3. Binary to URL (TTL: 300 = 5 minutes)
|
|
189
|
-
4. HTTP Request (send to processing API)
|
|
190
|
-
5. Binary to URL (operation: Delete)
|
|
191
100
|
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
```yaml
|
|
198
|
-
Workflow:
|
|
199
|
-
1. Webhook (trigger)
|
|
200
|
-
2. Generate Report
|
|
201
|
-
3. Binary to URL (TTL: 180 = 3 minutes)
|
|
202
|
-
4. Respond to Webhook (include proxyUrl in response)
|
|
101
|
+
1. Generate PDF report
|
|
102
|
+
2. Binary to URL (Upload, TTL: 600)
|
|
103
|
+
3. Send Email (use proxyUrl)
|
|
104
|
+
4. Binary to URL (Delete)
|
|
203
105
|
```
|
|
204
106
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
## Architecture
|
|
210
|
-
|
|
211
|
-
### In-Memory Storage Pattern
|
|
107
|
+
### Example 3: Batch Processing
|
|
212
108
|
|
|
213
109
|
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
│
|
|
220
|
-
│ GET Request (proxy file)
|
|
221
|
-
▼
|
|
222
|
-
┌─────────────────────────────────┐
|
|
223
|
-
│ Return file stream to client │
|
|
224
|
-
│ - Content-Type header │
|
|
225
|
-
│ - Cache-Control: 24h │
|
|
226
|
-
│ - Content-Disposition: inline │
|
|
227
|
-
└─────────────────────────────────┘
|
|
110
|
+
1. Read Binary Files
|
|
111
|
+
2. Split In Batches
|
|
112
|
+
3. Binary to URL (Upload, TTL: 300)
|
|
113
|
+
4. HTTP Request (send to API)
|
|
114
|
+
5. Binary to URL (Delete)
|
|
228
115
|
```
|
|
229
116
|
|
|
230
|
-
### Key Advantages
|
|
231
|
-
|
|
232
|
-
- **Zero External Dependencies** - No S3, no database, nothing to configure
|
|
233
|
-
- **Fast Performance** - In-memory storage is extremely fast
|
|
234
|
-
- **Automatic Cleanup** - Files expire automatically based on TTL
|
|
235
|
-
- **LRU Eviction** - Oldest files removed when cache is full
|
|
236
|
-
- **Secure File Keys** - Timestamp + random string prevents guessing
|
|
237
|
-
- **MIME Type Validation** - White-list of allowed file types for security
|
|
238
|
-
|
|
239
117
|
---
|
|
240
118
|
|
|
241
|
-
##
|
|
119
|
+
## Configuration
|
|
242
120
|
|
|
243
|
-
### Upload
|
|
121
|
+
### Upload Operation
|
|
244
122
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
contentType: string; // MIME type (e.g., "image/jpeg")
|
|
250
|
-
fileSize: number; // File size in bytes
|
|
251
|
-
}
|
|
252
|
-
```
|
|
123
|
+
| Parameter | Type | Default | Description |
|
|
124
|
+
|-----------|------|---------|-------------|
|
|
125
|
+
| Binary Property | string | `data` | Name of binary property |
|
|
126
|
+
| URL Expiration Time | number | `600` | TTL in seconds (60-604800) |
|
|
253
127
|
|
|
254
|
-
### Delete
|
|
128
|
+
### Delete Operation
|
|
255
129
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
deleted: string; // The file key that was deleted
|
|
260
|
-
}
|
|
261
|
-
```
|
|
130
|
+
| Parameter | Type | Required | Description |
|
|
131
|
+
|-----------|------|----------|-------------|
|
|
132
|
+
| File Key | string | Yes* | Key of file to delete |
|
|
262
133
|
|
|
263
|
-
|
|
134
|
+
*Can be provided from previous node's `fileKey`
|
|
264
135
|
|
|
265
|
-
|
|
266
|
-
- JPEG, PNG, GIF, WebP, SVG, BMP, TIFF, AVIF
|
|
136
|
+
### Storage Limits
|
|
267
137
|
|
|
268
|
-
|
|
269
|
-
|
|
138
|
+
| Limit | Value |
|
|
139
|
+
|-------|-------|
|
|
140
|
+
| Max file size | 100 MB |
|
|
141
|
+
| Max cache per workflow | 100 MB |
|
|
142
|
+
| Global max cache | 500 MB |
|
|
143
|
+
| Min TTL | 60 seconds |
|
|
144
|
+
| Max TTL | 604800 seconds (7 days) |
|
|
270
145
|
|
|
271
|
-
|
|
272
|
-
- MP3, WAV, OGG, FLAC
|
|
146
|
+
### Recommended TTL
|
|
273
147
|
|
|
274
|
-
**
|
|
275
|
-
-
|
|
148
|
+
- **60-300s** (1-5 min): Workflow-internal use
|
|
149
|
+
- **300-600s** (5-10 min): Short-term processing
|
|
150
|
+
- **600-3600s** (10-60 min): Longer operations
|
|
276
151
|
|
|
277
152
|
---
|
|
278
153
|
|
|
279
|
-
##
|
|
280
|
-
|
|
281
|
-
### File Type Validation
|
|
282
|
-
|
|
283
|
-
Files are validated against a white-list of allowed MIME types based on the provided MIME type from binary data.
|
|
284
|
-
|
|
285
|
-
### File Key Format
|
|
286
|
-
|
|
287
|
-
File keys follow the pattern: `{timestamp}-{random}`
|
|
288
|
-
|
|
289
|
-
Example: `1704801234567-abc123def456`
|
|
290
|
-
|
|
291
|
-
This prevents unauthorized file enumeration.
|
|
292
|
-
|
|
293
|
-
### File Size Limits
|
|
294
|
-
|
|
295
|
-
- **Maximum file size:** 100 MB
|
|
296
|
-
- **Maximum total cache:** 100 MB
|
|
297
|
-
- Configurable in source code (`MAX_FILE_SIZE` and `MAX_CACHE_SIZE` constants)
|
|
298
|
-
|
|
299
|
-
### Access Control
|
|
300
|
-
|
|
301
|
-
The webhook proxy inherits n8n's authentication and access control mechanisms.
|
|
302
|
-
|
|
303
|
-
### Automatic Expiration
|
|
154
|
+
## Supported File Types
|
|
304
155
|
|
|
305
|
-
|
|
156
|
+
| Category | Types |
|
|
157
|
+
|----------|-------|
|
|
158
|
+
| Images | JPEG, PNG, GIF, WebP, SVG, BMP, TIFF, AVIF |
|
|
159
|
+
| Videos | MP4, WebM, MOV, AVI, MKV |
|
|
160
|
+
| Audio | MP3, WAV, OGG, FLAC |
|
|
161
|
+
| Documents | PDF, ZIP, RAR, 7Z, TXT, CSV, JSON, XML, XLSX, DOCX |
|
|
306
162
|
|
|
307
163
|
---
|
|
308
164
|
|
|
309
165
|
## Troubleshooting
|
|
310
166
|
|
|
311
|
-
###
|
|
167
|
+
### Node not visible
|
|
312
168
|
|
|
313
|
-
|
|
169
|
+
1. Check installation: `npm list n8n-nodes-binary-to-url`
|
|
170
|
+
2. **Restart n8n** (most common issue)
|
|
171
|
+
3. Refresh browser page
|
|
314
172
|
|
|
315
|
-
|
|
173
|
+
### File URL returns 404
|
|
316
174
|
|
|
317
|
-
**
|
|
175
|
+
- Ensure workflow is **active** (webhooks only work in active workflows)
|
|
176
|
+
- Check if file expired (TTL passed)
|
|
177
|
+
- Verify fileKey is correct
|
|
178
|
+
- Try uploading again
|
|
318
179
|
|
|
319
|
-
|
|
320
|
-
- Check if the file has expired (TTL has passed)
|
|
321
|
-
- Ensure the fileKey is correct
|
|
322
|
-
- Try uploading the file again
|
|
180
|
+
### Cache full
|
|
323
181
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
**Problem:** Cache is at maximum capacity (100MB).
|
|
327
|
-
|
|
328
|
-
**Solution:**
|
|
329
|
-
|
|
330
|
-
- Wait for some files to expire
|
|
182
|
+
- Wait for files to expire
|
|
331
183
|
- Manually delete old files using Delete operation
|
|
332
|
-
- Increase
|
|
333
|
-
|
|
334
|
-
#### 3. Files Expire Too Quickly
|
|
335
|
-
|
|
336
|
-
**Problem:** Default TTL of 3600 seconds (1 hour) is too short.
|
|
337
|
-
|
|
338
|
-
**Solution:**
|
|
339
|
-
|
|
340
|
-
- Increase the TTL parameter when uploading (e.g., 86400 for 24 hours)
|
|
341
|
-
- Maximum recommended TTL: 604800 seconds (7 days)
|
|
184
|
+
- Increase cache size in source code if you have more RAM
|
|
342
185
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
**Problem:** n8n process is consuming too much memory.
|
|
346
|
-
|
|
347
|
-
**Solution:**
|
|
186
|
+
### Memory usage high
|
|
348
187
|
|
|
349
188
|
- Reduce TTL to expire files faster
|
|
350
189
|
- Reduce `MAX_CACHE_SIZE` in source code
|
|
351
|
-
-
|
|
190
|
+
- Delete files manually after use
|
|
352
191
|
|
|
353
192
|
---
|
|
354
193
|
|
|
355
|
-
##
|
|
356
|
-
|
|
357
|
-
### Project Structure
|
|
358
|
-
|
|
359
|
-
```
|
|
360
|
-
n8n-nodes-binary-to-url/
|
|
361
|
-
├── nodes/
|
|
362
|
-
│ └── BinaryToUrl/
|
|
363
|
-
│ ├── BinaryToUrl.node.ts # Main node implementation
|
|
364
|
-
│ └── BinaryToUrl.svg # Node icon
|
|
365
|
-
├── drivers/
|
|
366
|
-
│ ├── index.ts # Driver exports
|
|
367
|
-
│ └── MemoryStorage.ts # In-memory storage implementation
|
|
368
|
-
├── dist/ # Compiled output
|
|
369
|
-
├── package.json
|
|
370
|
-
├── tsconfig.json
|
|
371
|
-
└── README.md
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### Build
|
|
194
|
+
## Testing
|
|
375
195
|
|
|
376
|
-
|
|
377
|
-
npm install
|
|
378
|
-
npm run build
|
|
379
|
-
```
|
|
196
|
+
Create a test workflow:
|
|
380
197
|
|
|
381
|
-
|
|
198
|
+
1. **Manual Trigger** node
|
|
199
|
+
2. **HTTP Request** node: GET `https://picsum.photos/200/300`, Response Format: `File`
|
|
200
|
+
3. **Binary to URL** node: Upload, TTL: 600
|
|
201
|
+
4. **Save and activate** the workflow
|
|
202
|
+
5. **Execute** and copy the `proxyUrl`
|
|
203
|
+
6. **Open in browser** to verify
|
|
382
204
|
|
|
383
|
-
|
|
384
|
-
npm run dev # Watch mode
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Lint & Format
|
|
388
|
-
|
|
389
|
-
```bash
|
|
390
|
-
npm run lint # Check code quality
|
|
391
|
-
npm run lint:fix # Auto-fix lint issues
|
|
392
|
-
npm run format # Format with Prettier
|
|
393
|
-
```
|
|
205
|
+
**Expected result:** Image displays in browser.
|
|
394
206
|
|
|
395
207
|
---
|
|
396
208
|
|
|
397
|
-
##
|
|
209
|
+
## Links
|
|
398
210
|
|
|
399
|
-
- **
|
|
400
|
-
- **
|
|
401
|
-
- **n8n
|
|
402
|
-
- **Storage:** In-Memory (n8n process memory)
|
|
403
|
-
- **Dependencies:** None (zero external dependencies)
|
|
211
|
+
- **Technical Documentation**: [TECHNICAL.md](TECHNICAL.md)
|
|
212
|
+
- **Repository**: [https://cnb.cool/ksxh-wwrs/n8n-nodes-binary-to-url](https://cnb.cool/ksxh-wwrs/n8n-nodes-binary-to-url)
|
|
213
|
+
- **n8n Community**: [https://community.n8n.io](https://community.n8n.io)
|
|
404
214
|
|
|
405
215
|
---
|
|
406
216
|
|
|
407
217
|
## License
|
|
408
218
|
|
|
409
219
|
[MIT](LICENSE)
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
## Repository
|
|
414
|
-
|
|
415
|
-
[https://cnb.cool/ksxh-wwrs/n8n-nodes-binary-to-url](https://cnb.cool/ksxh-wwrs/n8n-nodes-binary-to-url)
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
## Contributing
|
|
420
|
-
|
|
421
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
422
|
-
|
|
423
|
-
---
|
|
424
|
-
|
|
425
|
-
## Support
|
|
426
|
-
|
|
427
|
-
- Create an issue in the GitHub repository
|
|
428
|
-
- Check the [n8n community forum](https://community.n8n.io)
|
|
429
|
-
|
|
430
|
-
---
|
|
431
|
-
|
|
432
|
-
## Changelog
|
|
433
|
-
|
|
434
|
-
### 0.0.9 (2026-01-10)
|
|
435
|
-
|
|
436
|
-
- Complete rewrite to use in-memory storage only
|
|
437
|
-
- Removed S3 and external storage dependencies
|
|
438
|
-
- Zero external dependencies
|
|
439
|
-
- Added automatic TTL-based cleanup
|
|
440
|
-
- Added LRU cache eviction
|
|
441
|
-
- Simplified configuration
|
|
442
|
-
- Improved performance with direct memory access
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
export declare class MemoryStorage {
|
|
2
|
-
private static
|
|
2
|
+
private static workflowCaches;
|
|
3
3
|
private static readonly DEFAULT_TTL;
|
|
4
4
|
private static readonly MAX_CACHE_SIZE;
|
|
5
|
-
private static
|
|
5
|
+
private static readonly GLOBAL_MAX_CACHE_SIZE;
|
|
6
|
+
private static globalCacheSize;
|
|
7
|
+
private static getOrCreateWorkflowCache;
|
|
6
8
|
static generateFileKey(): string;
|
|
7
|
-
static upload(data: Buffer, contentType: string, ttl?: number): Promise<{
|
|
9
|
+
static upload(workflowId: string, data: Buffer, contentType: string, ttl?: number): Promise<{
|
|
8
10
|
fileKey: string;
|
|
9
11
|
contentType: string;
|
|
10
12
|
}>;
|
|
11
|
-
static download(fileKey: string): Promise<{
|
|
13
|
+
static download(workflowId: string, fileKey: string): Promise<{
|
|
12
14
|
data: Buffer;
|
|
13
15
|
contentType: string;
|
|
14
16
|
} | null>;
|
|
15
|
-
static delete(fileKey: string): Promise<boolean>;
|
|
16
|
-
static
|
|
17
|
-
static
|
|
18
|
-
static
|
|
19
|
-
static
|
|
20
|
-
static
|
|
17
|
+
static delete(workflowId: string, fileKey: string): Promise<boolean>;
|
|
18
|
+
static cleanupWorkflowExpired(workflowId: string): void;
|
|
19
|
+
static cleanupAllExpired(): void;
|
|
20
|
+
static cleanupOldestInWorkflow(workflowId: string, requiredSpace: number): void;
|
|
21
|
+
static cleanupOldestGlobal(requiredSpace: number): void;
|
|
22
|
+
static getCacheSize(workflowId?: string): number;
|
|
23
|
+
static getCacheCount(workflowId?: string): number;
|
|
24
|
+
static clear(workflowId?: string): void;
|
|
21
25
|
}
|
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemoryStorage = void 0;
|
|
4
|
-
// Simple in-memory storage with TTL
|
|
5
4
|
class MemoryStorage {
|
|
5
|
+
static getOrCreateWorkflowCache(workflowId) {
|
|
6
|
+
if (!this.workflowCaches.has(workflowId)) {
|
|
7
|
+
this.workflowCaches.set(workflowId, {
|
|
8
|
+
cache: new Map(),
|
|
9
|
+
cacheSize: 0,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return this.workflowCaches.get(workflowId);
|
|
13
|
+
}
|
|
6
14
|
static generateFileKey() {
|
|
7
15
|
const timestamp = Date.now();
|
|
8
16
|
const random = Math.random().toString(36).substring(2, 15);
|
|
9
17
|
return `${timestamp}-${random}`;
|
|
10
18
|
}
|
|
11
|
-
static async upload(data, contentType, ttl) {
|
|
19
|
+
static async upload(workflowId, data, contentType, ttl) {
|
|
12
20
|
const fileKey = this.generateFileKey();
|
|
13
21
|
const now = Date.now();
|
|
14
22
|
const expiresAt = now + (ttl || this.DEFAULT_TTL);
|
|
15
23
|
const fileSize = data.length;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (this.globalCacheSize + fileSize > this.GLOBAL_MAX_CACHE_SIZE) {
|
|
25
|
+
this.cleanupAllExpired();
|
|
26
|
+
if (this.globalCacheSize + fileSize > this.GLOBAL_MAX_CACHE_SIZE) {
|
|
27
|
+
this.cleanupOldestGlobal(fileSize);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const workflowCache = this.getOrCreateWorkflowCache(workflowId);
|
|
31
|
+
if (workflowCache.cacheSize + fileSize > this.MAX_CACHE_SIZE) {
|
|
32
|
+
this.cleanupWorkflowExpired(workflowId);
|
|
33
|
+
if (workflowCache.cacheSize + fileSize > this.MAX_CACHE_SIZE) {
|
|
34
|
+
this.cleanupOldestInWorkflow(workflowId, fileSize);
|
|
23
35
|
}
|
|
24
36
|
}
|
|
25
37
|
const file = {
|
|
@@ -28,18 +40,22 @@ class MemoryStorage {
|
|
|
28
40
|
uploadedAt: now,
|
|
29
41
|
expiresAt,
|
|
30
42
|
};
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
workflowCache.cache.set(fileKey, file);
|
|
44
|
+
workflowCache.cacheSize += fileSize;
|
|
45
|
+
this.globalCacheSize += fileSize;
|
|
33
46
|
return { fileKey, contentType };
|
|
34
47
|
}
|
|
35
|
-
static async download(fileKey) {
|
|
36
|
-
const
|
|
48
|
+
static async download(workflowId, fileKey) {
|
|
49
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
50
|
+
if (!workflowCache) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const file = workflowCache.cache.get(fileKey);
|
|
37
54
|
if (!file) {
|
|
38
55
|
return null;
|
|
39
56
|
}
|
|
40
|
-
// Check if expired
|
|
41
57
|
if (Date.now() > file.expiresAt) {
|
|
42
|
-
this.delete(fileKey);
|
|
58
|
+
this.delete(workflowId, fileKey);
|
|
43
59
|
return null;
|
|
44
60
|
}
|
|
45
61
|
return {
|
|
@@ -47,46 +63,106 @@ class MemoryStorage {
|
|
|
47
63
|
contentType: file.contentType,
|
|
48
64
|
};
|
|
49
65
|
}
|
|
50
|
-
static async delete(fileKey) {
|
|
51
|
-
const
|
|
66
|
+
static async delete(workflowId, fileKey) {
|
|
67
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
68
|
+
if (!workflowCache)
|
|
69
|
+
return false;
|
|
70
|
+
const file = workflowCache.cache.get(fileKey);
|
|
52
71
|
if (!file)
|
|
53
72
|
return false;
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
workflowCache.cacheSize -= file.data.length;
|
|
74
|
+
this.globalCacheSize -= file.data.length;
|
|
75
|
+
return workflowCache.cache.delete(fileKey);
|
|
56
76
|
}
|
|
57
|
-
static
|
|
77
|
+
static cleanupWorkflowExpired(workflowId) {
|
|
78
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
79
|
+
if (!workflowCache)
|
|
80
|
+
return;
|
|
58
81
|
const now = Date.now();
|
|
59
|
-
for (const [key, file] of
|
|
82
|
+
for (const [key, file] of workflowCache.cache.entries()) {
|
|
60
83
|
if (now > file.expiresAt) {
|
|
61
|
-
this.delete(key);
|
|
84
|
+
this.delete(workflowId, key);
|
|
62
85
|
}
|
|
63
86
|
}
|
|
64
87
|
}
|
|
65
|
-
static
|
|
66
|
-
const
|
|
67
|
-
|
|
88
|
+
static cleanupAllExpired() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
for (const [workflowId, workflowCache] of this.workflowCaches.entries()) {
|
|
91
|
+
for (const [key, file] of workflowCache.cache.entries()) {
|
|
92
|
+
if (now > file.expiresAt) {
|
|
93
|
+
this.delete(workflowId, key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
static cleanupOldestInWorkflow(workflowId, requiredSpace) {
|
|
99
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
100
|
+
if (!workflowCache)
|
|
101
|
+
return;
|
|
102
|
+
const entries = Array.from(workflowCache.cache.entries());
|
|
68
103
|
entries.sort((a, b) => a[1].uploadedAt - b[1].uploadedAt);
|
|
69
104
|
let freedSpace = 0;
|
|
70
105
|
for (const [key, file] of entries) {
|
|
71
106
|
if (freedSpace >= requiredSpace)
|
|
72
107
|
break;
|
|
73
108
|
freedSpace += file.data.length;
|
|
74
|
-
this.delete(key);
|
|
109
|
+
this.delete(workflowId, key);
|
|
75
110
|
}
|
|
76
111
|
}
|
|
77
|
-
static
|
|
78
|
-
|
|
112
|
+
static cleanupOldestGlobal(requiredSpace) {
|
|
113
|
+
const allFiles = [];
|
|
114
|
+
for (const [workflowId, workflowCache] of this.workflowCaches.entries()) {
|
|
115
|
+
for (const [fileKey, file] of workflowCache.cache.entries()) {
|
|
116
|
+
allFiles.push({ workflowId, fileKey, file });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
allFiles.sort((a, b) => a.file.uploadedAt - b.file.uploadedAt);
|
|
120
|
+
let freedSpace = 0;
|
|
121
|
+
for (const { workflowId, fileKey, file } of allFiles) {
|
|
122
|
+
if (freedSpace >= requiredSpace)
|
|
123
|
+
break;
|
|
124
|
+
freedSpace += file.data.length;
|
|
125
|
+
this.delete(workflowId, fileKey);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
static getCacheSize(workflowId) {
|
|
129
|
+
if (workflowId) {
|
|
130
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
131
|
+
return workflowCache?.cacheSize ?? 0;
|
|
132
|
+
}
|
|
133
|
+
return this.globalCacheSize;
|
|
79
134
|
}
|
|
80
|
-
static getCacheCount() {
|
|
81
|
-
|
|
135
|
+
static getCacheCount(workflowId) {
|
|
136
|
+
if (workflowId) {
|
|
137
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
138
|
+
return workflowCache?.cache.size ?? 0;
|
|
139
|
+
}
|
|
140
|
+
let total = 0;
|
|
141
|
+
for (const workflowCache of this.workflowCaches.values()) {
|
|
142
|
+
total += workflowCache.cache.size;
|
|
143
|
+
}
|
|
144
|
+
return total;
|
|
82
145
|
}
|
|
83
|
-
static clear() {
|
|
84
|
-
|
|
85
|
-
|
|
146
|
+
static clear(workflowId) {
|
|
147
|
+
if (workflowId) {
|
|
148
|
+
const workflowCache = this.workflowCaches.get(workflowId);
|
|
149
|
+
if (workflowCache) {
|
|
150
|
+
for (const [, file] of workflowCache.cache.entries()) {
|
|
151
|
+
this.globalCacheSize -= file.data.length;
|
|
152
|
+
}
|
|
153
|
+
workflowCache.cache.clear();
|
|
154
|
+
workflowCache.cacheSize = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.workflowCaches.clear();
|
|
159
|
+
this.globalCacheSize = 0;
|
|
160
|
+
}
|
|
86
161
|
}
|
|
87
162
|
}
|
|
88
163
|
exports.MemoryStorage = MemoryStorage;
|
|
89
|
-
MemoryStorage.
|
|
90
|
-
MemoryStorage.DEFAULT_TTL = 60 * 60 * 1000;
|
|
91
|
-
MemoryStorage.MAX_CACHE_SIZE = 100 * 1024 * 1024;
|
|
92
|
-
MemoryStorage.
|
|
164
|
+
MemoryStorage.workflowCaches = new Map();
|
|
165
|
+
MemoryStorage.DEFAULT_TTL = 60 * 60 * 1000;
|
|
166
|
+
MemoryStorage.MAX_CACHE_SIZE = 100 * 1024 * 1024;
|
|
167
|
+
MemoryStorage.GLOBAL_MAX_CACHE_SIZE = 500 * 1024 * 1024;
|
|
168
|
+
MemoryStorage.globalCacheSize = 0;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BinaryToUrl = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
-
const
|
|
5
|
+
const MemoryStorage_js_1 = require("../../drivers/MemoryStorage.js");
|
|
6
6
|
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
7
7
|
const ALLOWED_MIME_TYPES = [
|
|
8
8
|
'image/jpeg',
|
|
@@ -54,7 +54,7 @@ class BinaryToUrl {
|
|
|
54
54
|
httpMethod: 'GET',
|
|
55
55
|
responseMode: 'onReceived',
|
|
56
56
|
path: 'file/:fileKey',
|
|
57
|
-
isFullPath:
|
|
57
|
+
isFullPath: false,
|
|
58
58
|
},
|
|
59
59
|
],
|
|
60
60
|
properties: [
|
|
@@ -134,6 +134,8 @@ class BinaryToUrl {
|
|
|
134
134
|
async webhook() {
|
|
135
135
|
const req = this.getRequestObject();
|
|
136
136
|
const fileKey = req.params.fileKey;
|
|
137
|
+
const workflow = this.getWorkflow();
|
|
138
|
+
const workflowId = workflow.id;
|
|
137
139
|
if (!fileKey) {
|
|
138
140
|
return {
|
|
139
141
|
webhookResponse: {
|
|
@@ -157,7 +159,7 @@ class BinaryToUrl {
|
|
|
157
159
|
};
|
|
158
160
|
}
|
|
159
161
|
try {
|
|
160
|
-
const result = await
|
|
162
|
+
const result = await MemoryStorage_js_1.MemoryStorage.download(workflowId, fileKey);
|
|
161
163
|
if (!result) {
|
|
162
164
|
return {
|
|
163
165
|
webhookResponse: {
|
|
@@ -172,7 +174,7 @@ class BinaryToUrl {
|
|
|
172
174
|
return {
|
|
173
175
|
webhookResponse: {
|
|
174
176
|
status: 200,
|
|
175
|
-
body: result.data
|
|
177
|
+
body: result.data,
|
|
176
178
|
headers: {
|
|
177
179
|
'Content-Type': result.contentType,
|
|
178
180
|
'Cache-Control': 'public, max-age=86400',
|
|
@@ -182,6 +184,7 @@ class BinaryToUrl {
|
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
catch (error) {
|
|
187
|
+
this.logger.error(`Error downloading file: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
188
|
return {
|
|
186
189
|
webhookResponse: {
|
|
187
190
|
status: 500,
|
|
@@ -198,20 +201,41 @@ exports.BinaryToUrl = BinaryToUrl;
|
|
|
198
201
|
async function handleUpload(context, items) {
|
|
199
202
|
const binaryPropertyName = context.getNodeParameter('binaryPropertyName', 0);
|
|
200
203
|
const ttl = context.getNodeParameter('ttl', 0);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
const MIN_TTL = 60;
|
|
205
|
+
const MAX_TTL = 604800;
|
|
206
|
+
if (ttl < MIN_TTL) {
|
|
207
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `TTL must be at least ${MIN_TTL} seconds. Got: ${ttl}`);
|
|
208
|
+
}
|
|
209
|
+
if (ttl > MAX_TTL) {
|
|
210
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `TTL cannot exceed ${MAX_TTL} seconds. Got: ${ttl}`);
|
|
211
|
+
}
|
|
204
212
|
const workflow = context.getWorkflow();
|
|
205
213
|
const workflowId = workflow.id;
|
|
206
|
-
const
|
|
214
|
+
const baseUrl = context.getInstanceBaseUrl();
|
|
215
|
+
// Remove trailing slash and ensure clean URL
|
|
216
|
+
const cleanBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
217
|
+
const webhookUrl = `${cleanBaseUrl}/webhook/${workflowId}/file/:fileKey`;
|
|
207
218
|
const returnData = [];
|
|
208
219
|
for (const item of items) {
|
|
209
220
|
const binaryData = item.binary?.[binaryPropertyName];
|
|
210
221
|
if (!binaryData) {
|
|
211
222
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `No binary data found in property "${binaryPropertyName}"`);
|
|
212
223
|
}
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
let buffer;
|
|
225
|
+
const data = binaryData.data;
|
|
226
|
+
if (Buffer.isBuffer(data)) {
|
|
227
|
+
buffer = data;
|
|
228
|
+
}
|
|
229
|
+
else if (typeof data === 'string') {
|
|
230
|
+
buffer = Buffer.from(data, 'base64');
|
|
231
|
+
}
|
|
232
|
+
else if (data && typeof data === 'object') {
|
|
233
|
+
const binaryValue = data.$binary || data;
|
|
234
|
+
buffer = Buffer.from(binaryValue, 'base64');
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Unsupported binary data format: ${typeof data}`);
|
|
238
|
+
}
|
|
215
239
|
const contentType = binaryData.mimeType || 'application/octet-stream';
|
|
216
240
|
if (!ALLOWED_MIME_TYPES.includes(contentType)) {
|
|
217
241
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `MIME type "${contentType}" is not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}`);
|
|
@@ -220,9 +244,9 @@ async function handleUpload(context, items) {
|
|
|
220
244
|
if (fileSize > MAX_FILE_SIZE) {
|
|
221
245
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `File size exceeds maximum limit of ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
222
246
|
}
|
|
223
|
-
const result = await
|
|
224
|
-
// Replace the :fileKey placeholder with the actual file key
|
|
247
|
+
const result = await MemoryStorage_js_1.MemoryStorage.upload(workflowId, buffer, contentType, ttl * 1000);
|
|
225
248
|
const proxyUrl = webhookUrl.replace(':fileKey', result.fileKey);
|
|
249
|
+
context.logger.info(`File uploaded: ${result.fileKey}, size: ${fileSize}, contentType: ${contentType}, TTL: ${ttl}s`);
|
|
226
250
|
returnData.push({
|
|
227
251
|
json: {
|
|
228
252
|
fileKey: result.fileKey,
|
|
@@ -236,16 +260,24 @@ async function handleUpload(context, items) {
|
|
|
236
260
|
return [returnData];
|
|
237
261
|
}
|
|
238
262
|
async function handleDelete(context, items) {
|
|
263
|
+
const workflow = context.getWorkflow();
|
|
264
|
+
const workflowId = workflow.id;
|
|
239
265
|
const returnData = [];
|
|
240
266
|
for (const item of items) {
|
|
241
267
|
const fileKey = (item.json.fileKey || context.getNodeParameter('fileKey', 0));
|
|
242
268
|
if (!fileKey) {
|
|
243
269
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'File key is required for delete operation');
|
|
244
270
|
}
|
|
245
|
-
await
|
|
271
|
+
const deleted = await MemoryStorage_js_1.MemoryStorage.delete(workflowId, fileKey);
|
|
272
|
+
if (deleted) {
|
|
273
|
+
context.logger.info(`File deleted: ${fileKey}`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
context.logger.warn(`File not found for deletion: ${fileKey}`);
|
|
277
|
+
}
|
|
246
278
|
returnData.push({
|
|
247
279
|
json: {
|
|
248
|
-
success:
|
|
280
|
+
success: deleted,
|
|
249
281
|
deleted: fileKey,
|
|
250
282
|
},
|
|
251
283
|
});
|
|
@@ -256,6 +288,6 @@ function isValidFileKey(fileKey) {
|
|
|
256
288
|
if (!fileKey || typeof fileKey !== 'string') {
|
|
257
289
|
return false;
|
|
258
290
|
}
|
|
259
|
-
const fileKeyPattern = /^[0-9]+-[a-z0-9]
|
|
291
|
+
const fileKeyPattern = /^[0-9]+-[a-z0-9]+$/i;
|
|
260
292
|
return fileKeyPattern.test(fileKey);
|
|
261
293
|
}
|
package/package.json
CHANGED
package/dist/drivers/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { MemoryStorage } from './MemoryStorage';
|
package/dist/drivers/index.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MemoryStorage = void 0;
|
|
4
|
-
var MemoryStorage_1 = require("./MemoryStorage");
|
|
5
|
-
Object.defineProperty(exports, "MemoryStorage", { enumerable: true, get: function () { return MemoryStorage_1.MemoryStorage; } });
|