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 CHANGED
@@ -1,27 +1,21 @@
1
- # n8n-nodes-binary-to-url
1
+ # Binary to URL - n8n Community Node
2
2
 
3
- <div align="center">
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
  [![npm version](https://badge.fury.io/js/n8n-nodes-binary-to-url.svg)](https://www.npmjs.com/package/n8n-nodes-binary-to-url)
10
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
7
 
12
- </div>
13
-
14
8
  ---
15
9
 
16
- ## ⚠️ Important Notice
10
+ ## Important Notice
17
11
 
18
- **This node is designed for temporary URL sharing within a workflow execution, NOT for long-term file storage or sharing.**
12
+ This node is designed for **temporary URL sharing within workflow execution**, NOT for long-term file storage.
19
13
 
20
- - ❌ **NOT** a file storage service
21
- - ❌ **NOT** for long-term URL sharing
22
- - ✅ **FOR** temporary URL passing between workflow nodes
23
- - ✅ **FOR** short-term external access (minutes to hours)
24
- - ✅ **FOR** workflow-internal binary data handling
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 without any external service
33
- - **Temporary URLs** - Create short-lived URLs for binary data access
34
- - **Zero Configuration** - No setup required, just upload and get URL
35
- - **Automatic Cleanup** - Files expire automatically (default: 10 minutes)
36
- - **Cache Management** - Built-in LRU cache with 100MB limit
37
- - **Workflow-Internal** - Designed for passing binary data between nodes
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
- ### Install in n8n
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
- ## Quick Start
43
+ ```bash
44
+ # If using npm
45
+ n8n restart
73
46
 
74
- ### Basic Usage
47
+ # If using Docker
48
+ docker-compose restart n8n
75
49
 
76
- This node creates temporary URLs for binary data that can be accessed within or outside the workflow.
50
+ # If using systemd
51
+ sudo systemctl restart n8n
52
+ ```
77
53
 
78
- **Use Case: Pass binary data between nodes**
54
+ ---
79
55
 
80
- ```yaml
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
- **Step-by-step:**
58
+ ### Upload Operation
89
59
 
90
60
  1. Add a **Binary to URL** node to your workflow
91
- 2. Select operation **Upload**
92
- 3. Set URL expiration time (default: 600 seconds = 10 minutes)
93
- 4. Connect any node with binary data (e.g., HTTP Request, Read Binary File)
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": "1704801234567-abc123def456",
101
- "proxyUrl": "https://your-n8n.com/webhook/123/file/1704801234567-abc123def456",
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
- **Use the URL:**
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
- **Note:** The URL is temporary and will stop working after the expiration time.
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
- ### Example 2: Temporary File for Email Attachment
167
-
168
- **Scenario:** Generate a PDF, send via email, then delete.
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
- ⚠️ **Warning:** Email recipients cannot access the file after TTL expires. For email attachments, consider using a proper file storage service.
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
- ### Example 4: Temporary Preview URL
194
-
195
- **Scenario:** Generate a temporary preview link for a webhook response.
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
- The URL will work for 3 minutes, then automatically expire.
206
-
207
- ---
208
-
209
- ## Architecture
210
-
211
- ### In-Memory Storage Pattern
107
+ ### Example 3: Batch Processing
212
108
 
213
109
  ```
214
- ┌─────────────┐ Upload ┌──────────────┐
215
- │ n8n Node │ ───────────────► │ Memory │
216
- │ (Binary ◄─────────────── │ Storage │
217
- │ to URL) │ Return URL │ (n8n RAM)
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
- ## API Reference
119
+ ## Configuration
242
120
 
243
- ### Upload Response
121
+ ### Upload Operation
244
122
 
245
- ```typescript
246
- {
247
- fileKey: string; // Unique file identifier
248
- proxyUrl: string; // Public URL to access file
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 Response
128
+ ### Delete Operation
255
129
 
256
- ```typescript
257
- {
258
- success: boolean; // true if deletion succeeded
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
- ### Supported File Types
134
+ *Can be provided from previous node's `fileKey`
264
135
 
265
- **Images:**
266
- - JPEG, PNG, GIF, WebP, SVG, BMP, TIFF, AVIF
136
+ ### Storage Limits
267
137
 
268
- **Videos:**
269
- - MP4, WebM, MOV, AVI, MKV
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
- **Audio:**
272
- - MP3, WAV, OGG, FLAC
146
+ ### Recommended TTL
273
147
 
274
- **Documents:**
275
- - PDF, ZIP, RAR, 7Z, TXT, CSV, JSON, XML, XLSX, DOCX
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
- ## Security
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
- Files are automatically deleted after their TTL expires. The default TTL is 3600 seconds (1 hour), but can be configured per upload.
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
- ### Common Issues
167
+ ### Node not visible
312
168
 
313
- #### 1. File Returns 404
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
- **Problem:** File not found or expired.
173
+ ### File URL returns 404
316
174
 
317
- **Solution:**
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
- - Verify the workflow is active (webhooks only work in active workflows)
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
- #### 2. "Cache Full" Warning
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 `MAX_CACHE_SIZE` in source code if you have more RAM available
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
- #### 4. Memory Usage Too High
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
- - Manually delete files after use instead of relying on auto-expiration
190
+ - Delete files manually after use
352
191
 
353
192
  ---
354
193
 
355
- ## Development
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
- ```bash
377
- npm install
378
- npm run build
379
- ```
196
+ Create a test workflow:
380
197
 
381
- ### Development
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
- ```bash
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
- ## Technical Details
209
+ ## Links
398
210
 
399
- - **Node Type:** Transform
400
- - **Version:** 0.0.9
401
- - **n8n Version:** >= 1.0.0
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 cache;
2
+ private static workflowCaches;
3
3
  private static readonly DEFAULT_TTL;
4
4
  private static readonly MAX_CACHE_SIZE;
5
- private static currentCacheSize;
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 cleanupExpired(): void;
17
- static cleanupOldest(requiredSpace: number): void;
18
- static getCacheSize(): number;
19
- static getCacheCount(): number;
20
- static clear(): void;
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
- // Check if adding this file would exceed max cache size
17
- if (this.currentCacheSize + fileSize > this.MAX_CACHE_SIZE) {
18
- // Clean up expired files first
19
- this.cleanupExpired();
20
- // If still too large, remove oldest files
21
- if (this.currentCacheSize + fileSize > this.MAX_CACHE_SIZE) {
22
- this.cleanupOldest(fileSize);
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
- this.cache.set(fileKey, file);
32
- this.currentCacheSize += fileSize;
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 file = this.cache.get(fileKey);
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 file = this.cache.get(fileKey);
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
- this.currentCacheSize -= file.data.length;
55
- return this.cache.delete(fileKey);
73
+ workflowCache.cacheSize -= file.data.length;
74
+ this.globalCacheSize -= file.data.length;
75
+ return workflowCache.cache.delete(fileKey);
56
76
  }
57
- static cleanupExpired() {
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 this.cache.entries()) {
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 cleanupOldest(requiredSpace) {
66
- const entries = Array.from(this.cache.entries());
67
- // Sort by upload time (oldest first)
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 getCacheSize() {
78
- return this.currentCacheSize;
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
- return this.cache.size;
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
- this.cache.clear();
85
- this.currentCacheSize = 0;
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.cache = new Map();
90
- MemoryStorage.DEFAULT_TTL = 60 * 60 * 1000; // 1 hour
91
- MemoryStorage.MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 MB
92
- MemoryStorage.currentCacheSize = 0;
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 MemoryStorage_1 = require("../../drivers/MemoryStorage");
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: true,
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 MemoryStorage_1.MemoryStorage.download(fileKey);
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.toString('base64'),
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
- // Build webhook URL using n8n's instance base URL and workflow ID
202
- // Format: {baseUrl}/webhook/{workflowId}/file/:fileKey
203
- const baseUrl = context.getInstanceBaseUrl();
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 webhookUrl = `${baseUrl}/webhook/${workflowId}/file/:fileKey`;
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
- const buffer = Buffer.from(binaryData.data, 'base64');
214
- // Use provided MIME type or default
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 MemoryStorage_1.MemoryStorage.upload(buffer, contentType, ttl);
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 MemoryStorage_1.MemoryStorage.delete(fileKey);
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: true,
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]+\.[a-z0-9]+$/i;
291
+ const fileKeyPattern = /^[0-9]+-[a-z0-9]+$/i;
260
292
  return fileKeyPattern.test(fileKey);
261
293
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-binary-to-url",
3
- "version": "0.0.11",
3
+ "version": "0.1.2",
4
4
  "description": "n8n community node for creating temporary URLs for binary files within workflow execution",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -1 +0,0 @@
1
- export { MemoryStorage } from './MemoryStorage';
@@ -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; } });