n8n-nodes-binary-to-url 0.0.4 → 0.0.6

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.
@@ -0,0 +1,15 @@
1
+ import type { Icon, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class S3StorageApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ icon: Icon;
6
+ documentationUrl: string;
7
+ properties: INodeProperties[];
8
+ test: {
9
+ request: {
10
+ baseURL: string;
11
+ url: string;
12
+ method: "GET";
13
+ };
14
+ };
15
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.S3StorageApi = void 0;
4
+ class S3StorageApi {
5
+ constructor() {
6
+ this.name = 's3StorageApi';
7
+ this.displayName = 'S3 Storage API';
8
+ this.icon = 'file:../icons/BinaryToUrl.svg';
9
+ this.documentationUrl = 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/AccessCredentials.html';
10
+ this.properties = [
11
+ {
12
+ displayName: 'Access Key ID',
13
+ name: 'accessKeyId',
14
+ type: 'string',
15
+ typeOptions: { password: true },
16
+ default: '',
17
+ },
18
+ {
19
+ displayName: 'Secret Access Key',
20
+ name: 'secretAccessKey',
21
+ type: 'string',
22
+ typeOptions: { password: true },
23
+ default: '',
24
+ },
25
+ ];
26
+ this.test = {
27
+ request: {
28
+ baseURL: '={{$credentials.endpoint}}',
29
+ url: '=/',
30
+ method: 'GET',
31
+ },
32
+ };
33
+ }
34
+ }
35
+ exports.S3StorageApi = S3StorageApi;
@@ -1,6 +1,72 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.S3Storage = void 0;
37
+ // Use Node.js crypto in Node environment, Web Crypto API in browser
38
+ const crypto = __importStar(require("node:crypto"));
39
+ let cryptoInstance;
40
+ if (typeof window !== 'undefined' && window?.crypto) {
41
+ // Browser environment (n8n Cloud)
42
+ cryptoInstance = window.crypto;
43
+ }
44
+ else {
45
+ // Node.js environment
46
+ // Create a Web Crypto API compatible wrapper
47
+ cryptoInstance = {
48
+ subtle: {
49
+ digest: async (algorithm, data) => {
50
+ const hash = crypto.createHash(algorithm.replace('-', '').toLowerCase());
51
+ hash.update(Buffer.from(data));
52
+ return Buffer.from(hash.digest()).buffer;
53
+ },
54
+ importKey: async (format, keyData, algorithm, extractable, usages) => {
55
+ return {
56
+ algorithm,
57
+ extractable,
58
+ usages,
59
+ data: format === 'raw' ? keyData : keyData,
60
+ };
61
+ },
62
+ sign: async (algorithm, key, data) => {
63
+ const hmac = crypto.createHmac('sha256', key.data);
64
+ hmac.update(Buffer.from(data));
65
+ return Buffer.from(hmac.digest()).buffer;
66
+ },
67
+ },
68
+ };
69
+ }
4
70
  class S3Storage {
5
71
  constructor(config) {
6
72
  this.config = config;
@@ -163,15 +229,15 @@ class S3Storage {
163
229
  async sha256(message) {
164
230
  const encoder = new TextEncoder();
165
231
  const data = encoder.encode(message);
166
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
232
+ const hashBuffer = await cryptoInstance.subtle.digest('SHA-256', data);
167
233
  const hashArray = Array.from(new Uint8Array(hashBuffer));
168
234
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
169
235
  }
170
236
  async hmac(key, message) {
171
- const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
237
+ const cryptoKey = await cryptoInstance.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
172
238
  const encoder = new TextEncoder();
173
239
  const data = encoder.encode(message);
174
- const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
240
+ const signature = await cryptoInstance.subtle.sign('HMAC', cryptoKey, data);
175
241
  const signatureArray = Array.from(new Uint8Array(signature));
176
242
  return signatureArray.map((b) => b.toString(16).padStart(2, '0')).join('');
177
243
  }
@@ -186,8 +252,8 @@ class S3Storage {
186
252
  const keyBuffer = typeof key === 'string' ? Buffer.from(key) : key;
187
253
  const encoder = new TextEncoder();
188
254
  const data = encoder.encode(message);
189
- const cryptoKey = await crypto.subtle.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
190
- const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
255
+ const cryptoKey = await cryptoInstance.subtle.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
256
+ const signature = await cryptoInstance.subtle.sign('HMAC', cryptoKey, data);
191
257
  return Buffer.from(signature);
192
258
  }
193
259
  generateFileKey(contentType) {
@@ -6,7 +6,7 @@ const S3Storage_1 = require("./S3Storage");
6
6
  var S3Storage_2 = require("./S3Storage");
7
7
  Object.defineProperty(exports, "S3Storage", { enumerable: true, get: function () { return S3Storage_2.S3Storage; } });
8
8
  async function createStorageDriver(context, bucket) {
9
- const credentials = await context.getCredentials('awsS3');
9
+ const credentials = await context.getCredentials('s3StorageApi');
10
10
  if (!credentials) {
11
11
  throw new Error('No S3 credentials found. Please configure S3 credentials.');
12
12
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { BinaryBridge } from './nodes/BinaryBridge/BinaryBridge.node';
2
- export declare const nodeClasses: (typeof BinaryBridge)[];
1
+ import { BinaryToUrl } from './nodes/BinaryToUrl/BinaryToUrl.node';
2
+ export declare const nodeClasses: (typeof BinaryToUrl)[];
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nodeClasses = void 0;
4
- const BinaryBridge_node_1 = require("./nodes/BinaryBridge/BinaryBridge.node");
5
- exports.nodeClasses = [BinaryBridge_node_1.BinaryBridge];
4
+ const BinaryToUrl_node_1 = require("./nodes/BinaryToUrl/BinaryToUrl.node");
5
+ exports.nodeClasses = [BinaryToUrl_node_1.BinaryToUrl];
@@ -1,5 +1,5 @@
1
1
  import { INodeType, INodeTypeDescription, IExecuteFunctions, IWebhookFunctions, IWebhookResponseData, INodeExecutionData } from 'n8n-workflow';
2
- export declare class BinaryBridge implements INodeType {
2
+ export declare class BinaryToUrl implements INodeType {
3
3
  description: INodeTypeDescription;
4
4
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
5
  webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BinaryBridge = void 0;
3
+ exports.BinaryToUrl = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const drivers_1 = require("../../drivers");
6
6
  const MAX_FILE_SIZE = 100 * 1024 * 1024;
@@ -33,7 +33,7 @@ const ALLOWED_MIME_TYPES = [
33
33
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
34
34
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
35
35
  ];
36
- class BinaryBridge {
36
+ class BinaryToUrl {
37
37
  constructor() {
38
38
  this.description = {
39
39
  displayName: 'Binary to URL',
@@ -50,7 +50,7 @@ class BinaryBridge {
50
50
  outputs: ['main'],
51
51
  credentials: [
52
52
  {
53
- name: 'awsS3',
53
+ name: 's3StorageApi',
54
54
  required: true,
55
55
  },
56
56
  ],
@@ -142,9 +142,10 @@ class BinaryBridge {
142
142
  name: 'forcePathStyle',
143
143
  type: 'boolean',
144
144
  default: false,
145
- description: 'Use path-style addressing (required for MinIO, DigitalOcean Spaces, etc.)',
145
+ description: 'Whether to use path-style addressing (required for MinIO, DigitalOcean Spaces, etc.)',
146
146
  },
147
147
  ],
148
+ usableAsTool: true,
148
149
  };
149
150
  }
150
151
  async execute() {
@@ -237,7 +238,7 @@ class BinaryBridge {
237
238
  },
238
239
  };
239
240
  }
240
- catch (error) {
241
+ catch {
241
242
  return {
242
243
  webhookResponse: {
243
244
  status: 404,
@@ -250,7 +251,7 @@ class BinaryBridge {
250
251
  }
251
252
  }
252
253
  }
253
- exports.BinaryBridge = BinaryBridge;
254
+ exports.BinaryToUrl = BinaryToUrl;
254
255
  async function handleUpload(context, items, storage) {
255
256
  const binaryPropertyName = context.getNodeParameter('binaryPropertyName', 0);
256
257
  const webhookBaseUrl = buildWebhookUrl(context, 'default', 'file');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-binary-to-url",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "n8n community node for binary file to public URL bridge with S3 storage",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -24,37 +24,35 @@
24
24
  },
25
25
  "main": "dist/index.js",
26
26
  "scripts": {
27
- "build": "tsc && cp -r nodes dist/",
28
- "dev": "tsc --watch",
27
+ "build": "n8n-node build",
28
+ "build:watch": "tsc --watch",
29
+ "dev": "n8n-node dev",
29
30
  "format": "prettier --write \"**/*.{js,ts,json,md}\"",
30
- "lint": "eslint --ext .js,.ts .",
31
- "lintfix": "eslint --fix --ext .js,.ts .",
32
- "prepublishOnly": "npm run build",
33
- "test": "jest"
31
+ "lint": "n8n-node lint",
32
+ "lint:fix": "n8n-node lint --fix",
33
+ "release": "n8n-node release",
34
+ "prepublishOnly": "n8n-node prerelease"
34
35
  },
35
36
  "files": [
36
- "dist",
37
- "nodes"
37
+ "dist"
38
38
  ],
39
39
  "n8n": {
40
40
  "n8nNodesApiVersion": 1,
41
- "credentials": [],
41
+ "strict": true,
42
+ "credentials": [
43
+ "dist/credentials/S3StorageApi.credentials.js"
44
+ ],
42
45
  "nodes": [
43
- "dist/nodes/BinaryBridge/BinaryBridge.node.js"
46
+ "dist/nodes/BinaryToUrl/BinaryToUrl.node.js"
44
47
  ]
45
48
  },
46
49
  "dependencies": {},
47
50
  "devDependencies": {
48
- "@types/jest": "^30.0.0",
49
- "@types/node": "^20.0.0",
50
- "@typescript-eslint/eslint-plugin": "^6.0.0",
51
- "@typescript-eslint/parser": "^6.0.0",
52
- "eslint": "^8.50.0",
51
+ "@n8n/node-cli": "*",
52
+ "eslint": "9.32.0",
53
53
  "eslint-plugin-n8n-nodes-base": "^1.11.0",
54
- "jest": "^30.2.0",
55
- "prettier": "^3.0.0",
56
- "ts-jest": "^29.4.6",
57
- "typescript": "^5.2.0"
54
+ "prettier": "3.6.2",
55
+ "typescript": "5.9.2"
58
56
  },
59
57
  "peerDependencies": {
60
58
  "n8n-workflow": "*"
@@ -1,370 +0,0 @@
1
- import {
2
- INodeType,
3
- INodeTypeDescription,
4
- IExecuteFunctions,
5
- IWebhookFunctions,
6
- IWebhookResponseData,
7
- INodeExecutionData,
8
- NodeOperationError,
9
- } from 'n8n-workflow';
10
- import { createStorageDriver, StorageDriver } from '../../drivers';
11
-
12
- const MAX_FILE_SIZE = 100 * 1024 * 1024;
13
- const ALLOWED_MIME_TYPES = [
14
- 'image/jpeg',
15
- 'image/png',
16
- 'image/gif',
17
- 'image/webp',
18
- 'image/svg+xml',
19
- 'image/bmp',
20
- 'image/tiff',
21
- 'image/avif',
22
- 'video/mp4',
23
- 'video/webm',
24
- 'video/quicktime',
25
- 'video/x-msvideo',
26
- 'video/x-matroska',
27
- 'application/pdf',
28
- 'application/zip',
29
- 'application/x-rar-compressed',
30
- 'application/x-7z-compressed',
31
- 'audio/mpeg',
32
- 'audio/wav',
33
- 'audio/ogg',
34
- 'audio/flac',
35
- 'text/plain',
36
- 'text/csv',
37
- 'application/json',
38
- 'application/xml',
39
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
41
- ];
42
-
43
- export class BinaryBridge implements INodeType {
44
- description: INodeTypeDescription = {
45
- displayName: 'Binary to URL',
46
- name: 'binaryToUrl',
47
- icon: 'file:BinaryBridge.svg',
48
- group: ['transform'],
49
- version: 1,
50
- subtitle: '={{$parameter["operation"]}}',
51
- description: 'Upload binary files to S3 storage and proxy them via public URL',
52
- defaults: {
53
- name: 'Binary to URL',
54
- },
55
- inputs: ['main'],
56
- outputs: ['main'],
57
- credentials: [
58
- {
59
- name: 'awsS3',
60
- required: true,
61
- },
62
- ],
63
- webhooks: [
64
- {
65
- name: 'default',
66
- httpMethod: 'GET',
67
- responseMode: 'onReceived',
68
- path: 'file/:fileKey',
69
- isFullPath: true,
70
- },
71
- ],
72
- properties: [
73
- {
74
- displayName: 'Operation',
75
- name: 'operation',
76
- type: 'options',
77
- noDataExpression: true,
78
- options: [
79
- {
80
- name: 'Upload',
81
- value: 'upload',
82
- description: 'Upload binary file to storage',
83
- action: 'Upload file',
84
- },
85
- {
86
- name: 'Delete',
87
- value: 'delete',
88
- description: 'Delete file from storage',
89
- action: 'Delete file',
90
- },
91
- ],
92
- default: 'upload',
93
- },
94
- {
95
- displayName: 'Binary Property',
96
- name: 'binaryPropertyName',
97
- type: 'string',
98
- displayOptions: {
99
- show: {
100
- operation: ['upload'],
101
- },
102
- },
103
- default: 'data',
104
- description: 'Name of binary property containing the file to upload',
105
- },
106
- {
107
- displayName: 'File Key',
108
- name: 'fileKey',
109
- type: 'string',
110
- displayOptions: {
111
- show: {
112
- operation: ['delete'],
113
- },
114
- },
115
- default: '',
116
- description: 'Key of the file to delete from storage',
117
- },
118
- {
119
- displayName: 'Bucket',
120
- name: 'bucket',
121
- type: 'string',
122
- default: '',
123
- required: true,
124
- description: 'Storage bucket name',
125
- },
126
- {
127
- displayName: 'Region',
128
- name: 'region',
129
- type: 'string',
130
- default: 'us-east-1',
131
- required: true,
132
- description: 'AWS region (leave empty for some S3-compatible services)',
133
- },
134
- {
135
- displayName: 'Custom Endpoint',
136
- name: 'endpoint',
137
- type: 'string',
138
- default: '',
139
- description:
140
- 'Custom S3 endpoint URL (required for MinIO, DigitalOcean Spaces, Wasabi, etc.)',
141
- displayOptions: {
142
- show: {
143
- operation: ['upload', 'delete'],
144
- },
145
- },
146
- },
147
- {
148
- displayName: 'Force Path Style',
149
- name: 'forcePathStyle',
150
- type: 'boolean',
151
- default: false,
152
- description: 'Use path-style addressing (required for MinIO, DigitalOcean Spaces, etc.)',
153
- },
154
- ],
155
- };
156
-
157
- async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
158
- const items = this.getInputData();
159
- const operation = this.getNodeParameter('operation', 0) as string;
160
- const bucket = this.getNodeParameter('bucket', 0) as string;
161
-
162
- if (!bucket) {
163
- throw new NodeOperationError(this.getNode(), 'Bucket name is required');
164
- }
165
-
166
- try {
167
- const storage = await createStorageDriver(this, bucket);
168
-
169
- if (operation === 'upload') {
170
- return handleUpload(this, items, storage);
171
- } else if (operation === 'delete') {
172
- return handleDelete(this, items, storage);
173
- }
174
-
175
- throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
176
- } catch (error) {
177
- if (error instanceof Error) {
178
- throw new NodeOperationError(this.getNode(), `Operation failed: ${error.message}`);
179
- }
180
- throw new NodeOperationError(this.getNode(), `Operation failed: ${String(error)}`);
181
- }
182
- }
183
-
184
- async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
185
- const req = this.getRequestObject();
186
- const fileKey = req.params.fileKey as string;
187
-
188
- if (!fileKey) {
189
- return {
190
- webhookResponse: {
191
- status: 400,
192
- body: JSON.stringify({ error: 'Missing fileKey' }),
193
- headers: {
194
- 'Content-Type': 'application/json',
195
- },
196
- },
197
- };
198
- }
199
-
200
- if (!isValidFileKey(fileKey)) {
201
- return {
202
- webhookResponse: {
203
- status: 400,
204
- body: JSON.stringify({ error: 'Invalid fileKey' }),
205
- headers: {
206
- 'Content-Type': 'application/json',
207
- },
208
- },
209
- };
210
- }
211
-
212
- const bucket = this.getNodeParameter('bucket', 0) as string;
213
-
214
- if (!bucket) {
215
- return {
216
- webhookResponse: {
217
- status: 500,
218
- body: JSON.stringify({ error: 'Node configuration is incomplete' }),
219
- headers: {
220
- 'Content-Type': 'application/json',
221
- },
222
- },
223
- };
224
- }
225
-
226
- let storage;
227
- try {
228
- storage = await createStorageDriver(this, bucket);
229
- } catch (error) {
230
- return {
231
- webhookResponse: {
232
- status: 500,
233
- body: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }),
234
- headers: {
235
- 'Content-Type': 'application/json',
236
- },
237
- },
238
- };
239
- }
240
-
241
- try {
242
- const { data, contentType } = await storage.downloadStream(fileKey);
243
-
244
- return {
245
- webhookResponse: {
246
- status: 200,
247
- body: data.toString('base64'),
248
- headers: {
249
- 'Content-Type': contentType,
250
- 'Cache-Control': 'public, max-age=86400',
251
- 'Content-Disposition': 'inline',
252
- },
253
- },
254
- };
255
- } catch (error) {
256
- return {
257
- webhookResponse: {
258
- status: 404,
259
- body: JSON.stringify({ error: 'File not found' }),
260
- headers: {
261
- 'Content-Type': 'application/json',
262
- },
263
- },
264
- };
265
- }
266
- }
267
- }
268
-
269
- async function handleUpload(
270
- context: IExecuteFunctions,
271
- items: INodeExecutionData[],
272
- storage: StorageDriver
273
- ): Promise<INodeExecutionData[][]> {
274
- const binaryPropertyName = context.getNodeParameter('binaryPropertyName', 0) as string;
275
- const webhookBaseUrl = buildWebhookUrl(context, 'default', 'file');
276
-
277
- const returnData: INodeExecutionData[] = [];
278
-
279
- for (const item of items) {
280
- const binaryData = item.binary?.[binaryPropertyName];
281
-
282
- if (!binaryData) {
283
- throw new NodeOperationError(
284
- context.getNode(),
285
- `No binary data found in property "${binaryPropertyName}"`
286
- );
287
- }
288
-
289
- const buffer = Buffer.from(binaryData.data, 'base64');
290
-
291
- // Use provided MIME type or default
292
- const contentType = binaryData.mimeType || 'application/octet-stream';
293
-
294
- if (!ALLOWED_MIME_TYPES.includes(contentType)) {
295
- throw new NodeOperationError(
296
- context.getNode(),
297
- `MIME type "${contentType}" is not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}`
298
- );
299
- }
300
-
301
- const fileSize = buffer.length;
302
- if (fileSize > MAX_FILE_SIZE) {
303
- throw new NodeOperationError(
304
- context.getNode(),
305
- `File size exceeds maximum limit of ${MAX_FILE_SIZE / 1024 / 1024}MB`
306
- );
307
- }
308
-
309
- const result = await storage.uploadStream(buffer, contentType);
310
-
311
- const proxyUrl = `${webhookBaseUrl}/${result.fileKey}`;
312
-
313
- returnData.push({
314
- json: {
315
- fileKey: result.fileKey,
316
- proxyUrl,
317
- contentType,
318
- fileSize,
319
- },
320
- binary: item.binary,
321
- });
322
- }
323
-
324
- return [returnData];
325
- }
326
-
327
- async function handleDelete(
328
- context: IExecuteFunctions,
329
- items: INodeExecutionData[],
330
- storage: StorageDriver
331
- ): Promise<INodeExecutionData[][]> {
332
- const returnData: INodeExecutionData[] = [];
333
-
334
- for (const item of items) {
335
- const fileKey = (item.json.fileKey || context.getNodeParameter('fileKey', 0)) as string;
336
-
337
- if (!fileKey) {
338
- throw new NodeOperationError(context.getNode(), 'File key is required for delete operation');
339
- }
340
-
341
- await storage.deleteFile(fileKey);
342
-
343
- returnData.push({
344
- json: {
345
- success: true,
346
- deleted: fileKey,
347
- },
348
- });
349
- }
350
-
351
- return [returnData];
352
- }
353
-
354
- function buildWebhookUrl(context: IExecuteFunctions, webhookName: string, path: string): string {
355
- const baseUrl = context.getInstanceBaseUrl();
356
- const node = context.getNode();
357
- const workflow = context.getWorkflow();
358
- const workflowId = workflow.id;
359
- const nodeName = encodeURIComponent(node.name.toLowerCase());
360
- return `${baseUrl}/webhook/${workflowId}/${nodeName}/${path}`;
361
- }
362
-
363
- function isValidFileKey(fileKey: string): boolean {
364
- if (!fileKey || typeof fileKey !== 'string') {
365
- return false;
366
- }
367
-
368
- const fileKeyPattern = /^[0-9]+-[a-z0-9]+\.[a-z0-9]+$/i;
369
- return fileKeyPattern.test(fileKey);
370
- }
@@ -1,370 +0,0 @@
1
- import {
2
- INodeType,
3
- INodeTypeDescription,
4
- IExecuteFunctions,
5
- IWebhookFunctions,
6
- IWebhookResponseData,
7
- INodeExecutionData,
8
- NodeOperationError,
9
- } from 'n8n-workflow';
10
- import { createStorageDriver, StorageDriver } from '../../drivers';
11
-
12
- const MAX_FILE_SIZE = 100 * 1024 * 1024;
13
- const ALLOWED_MIME_TYPES = [
14
- 'image/jpeg',
15
- 'image/png',
16
- 'image/gif',
17
- 'image/webp',
18
- 'image/svg+xml',
19
- 'image/bmp',
20
- 'image/tiff',
21
- 'image/avif',
22
- 'video/mp4',
23
- 'video/webm',
24
- 'video/quicktime',
25
- 'video/x-msvideo',
26
- 'video/x-matroska',
27
- 'application/pdf',
28
- 'application/zip',
29
- 'application/x-rar-compressed',
30
- 'application/x-7z-compressed',
31
- 'audio/mpeg',
32
- 'audio/wav',
33
- 'audio/ogg',
34
- 'audio/flac',
35
- 'text/plain',
36
- 'text/csv',
37
- 'application/json',
38
- 'application/xml',
39
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
41
- ];
42
-
43
- export class BinaryBridge implements INodeType {
44
- description: INodeTypeDescription = {
45
- displayName: 'Binary to URL',
46
- name: 'binaryToUrl',
47
- icon: 'file:BinaryBridge.svg',
48
- group: ['transform'],
49
- version: 1,
50
- subtitle: '={{$parameter["operation"]}}',
51
- description: 'Upload binary files to S3 storage and proxy them via public URL',
52
- defaults: {
53
- name: 'Binary to URL',
54
- },
55
- inputs: ['main'],
56
- outputs: ['main'],
57
- credentials: [
58
- {
59
- name: 'awsS3',
60
- required: true,
61
- },
62
- ],
63
- webhooks: [
64
- {
65
- name: 'default',
66
- httpMethod: 'GET',
67
- responseMode: 'onReceived',
68
- path: 'file/:fileKey',
69
- isFullPath: true,
70
- },
71
- ],
72
- properties: [
73
- {
74
- displayName: 'Operation',
75
- name: 'operation',
76
- type: 'options',
77
- noDataExpression: true,
78
- options: [
79
- {
80
- name: 'Upload',
81
- value: 'upload',
82
- description: 'Upload binary file to storage',
83
- action: 'Upload file',
84
- },
85
- {
86
- name: 'Delete',
87
- value: 'delete',
88
- description: 'Delete file from storage',
89
- action: 'Delete file',
90
- },
91
- ],
92
- default: 'upload',
93
- },
94
- {
95
- displayName: 'Binary Property',
96
- name: 'binaryPropertyName',
97
- type: 'string',
98
- displayOptions: {
99
- show: {
100
- operation: ['upload'],
101
- },
102
- },
103
- default: 'data',
104
- description: 'Name of binary property containing the file to upload',
105
- },
106
- {
107
- displayName: 'File Key',
108
- name: 'fileKey',
109
- type: 'string',
110
- displayOptions: {
111
- show: {
112
- operation: ['delete'],
113
- },
114
- },
115
- default: '',
116
- description: 'Key of the file to delete from storage',
117
- },
118
- {
119
- displayName: 'Bucket',
120
- name: 'bucket',
121
- type: 'string',
122
- default: '',
123
- required: true,
124
- description: 'Storage bucket name',
125
- },
126
- {
127
- displayName: 'Region',
128
- name: 'region',
129
- type: 'string',
130
- default: 'us-east-1',
131
- required: true,
132
- description: 'AWS region (leave empty for some S3-compatible services)',
133
- },
134
- {
135
- displayName: 'Custom Endpoint',
136
- name: 'endpoint',
137
- type: 'string',
138
- default: '',
139
- description:
140
- 'Custom S3 endpoint URL (required for MinIO, DigitalOcean Spaces, Wasabi, etc.)',
141
- displayOptions: {
142
- show: {
143
- operation: ['upload', 'delete'],
144
- },
145
- },
146
- },
147
- {
148
- displayName: 'Force Path Style',
149
- name: 'forcePathStyle',
150
- type: 'boolean',
151
- default: false,
152
- description: 'Use path-style addressing (required for MinIO, DigitalOcean Spaces, etc.)',
153
- },
154
- ],
155
- };
156
-
157
- async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
158
- const items = this.getInputData();
159
- const operation = this.getNodeParameter('operation', 0) as string;
160
- const bucket = this.getNodeParameter('bucket', 0) as string;
161
-
162
- if (!bucket) {
163
- throw new NodeOperationError(this.getNode(), 'Bucket name is required');
164
- }
165
-
166
- try {
167
- const storage = await createStorageDriver(this, bucket);
168
-
169
- if (operation === 'upload') {
170
- return handleUpload(this, items, storage);
171
- } else if (operation === 'delete') {
172
- return handleDelete(this, items, storage);
173
- }
174
-
175
- throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
176
- } catch (error) {
177
- if (error instanceof Error) {
178
- throw new NodeOperationError(this.getNode(), `Operation failed: ${error.message}`);
179
- }
180
- throw new NodeOperationError(this.getNode(), `Operation failed: ${String(error)}`);
181
- }
182
- }
183
-
184
- async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
185
- const req = this.getRequestObject();
186
- const fileKey = req.params.fileKey as string;
187
-
188
- if (!fileKey) {
189
- return {
190
- webhookResponse: {
191
- status: 400,
192
- body: JSON.stringify({ error: 'Missing fileKey' }),
193
- headers: {
194
- 'Content-Type': 'application/json',
195
- },
196
- },
197
- };
198
- }
199
-
200
- if (!isValidFileKey(fileKey)) {
201
- return {
202
- webhookResponse: {
203
- status: 400,
204
- body: JSON.stringify({ error: 'Invalid fileKey' }),
205
- headers: {
206
- 'Content-Type': 'application/json',
207
- },
208
- },
209
- };
210
- }
211
-
212
- const bucket = this.getNodeParameter('bucket', 0) as string;
213
-
214
- if (!bucket) {
215
- return {
216
- webhookResponse: {
217
- status: 500,
218
- body: JSON.stringify({ error: 'Node configuration is incomplete' }),
219
- headers: {
220
- 'Content-Type': 'application/json',
221
- },
222
- },
223
- };
224
- }
225
-
226
- let storage;
227
- try {
228
- storage = await createStorageDriver(this, bucket);
229
- } catch (error) {
230
- return {
231
- webhookResponse: {
232
- status: 500,
233
- body: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }),
234
- headers: {
235
- 'Content-Type': 'application/json',
236
- },
237
- },
238
- };
239
- }
240
-
241
- try {
242
- const { data, contentType } = await storage.downloadStream(fileKey);
243
-
244
- return {
245
- webhookResponse: {
246
- status: 200,
247
- body: data.toString('base64'),
248
- headers: {
249
- 'Content-Type': contentType,
250
- 'Cache-Control': 'public, max-age=86400',
251
- 'Content-Disposition': 'inline',
252
- },
253
- },
254
- };
255
- } catch (error) {
256
- return {
257
- webhookResponse: {
258
- status: 404,
259
- body: JSON.stringify({ error: 'File not found' }),
260
- headers: {
261
- 'Content-Type': 'application/json',
262
- },
263
- },
264
- };
265
- }
266
- }
267
- }
268
-
269
- async function handleUpload(
270
- context: IExecuteFunctions,
271
- items: INodeExecutionData[],
272
- storage: StorageDriver
273
- ): Promise<INodeExecutionData[][]> {
274
- const binaryPropertyName = context.getNodeParameter('binaryPropertyName', 0) as string;
275
- const webhookBaseUrl = buildWebhookUrl(context, 'default', 'file');
276
-
277
- const returnData: INodeExecutionData[] = [];
278
-
279
- for (const item of items) {
280
- const binaryData = item.binary?.[binaryPropertyName];
281
-
282
- if (!binaryData) {
283
- throw new NodeOperationError(
284
- context.getNode(),
285
- `No binary data found in property "${binaryPropertyName}"`
286
- );
287
- }
288
-
289
- const buffer = Buffer.from(binaryData.data, 'base64');
290
-
291
- // Use provided MIME type or default
292
- const contentType = binaryData.mimeType || 'application/octet-stream';
293
-
294
- if (!ALLOWED_MIME_TYPES.includes(contentType)) {
295
- throw new NodeOperationError(
296
- context.getNode(),
297
- `MIME type "${contentType}" is not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}`
298
- );
299
- }
300
-
301
- const fileSize = buffer.length;
302
- if (fileSize > MAX_FILE_SIZE) {
303
- throw new NodeOperationError(
304
- context.getNode(),
305
- `File size exceeds maximum limit of ${MAX_FILE_SIZE / 1024 / 1024}MB`
306
- );
307
- }
308
-
309
- const result = await storage.uploadStream(buffer, contentType);
310
-
311
- const proxyUrl = `${webhookBaseUrl}/${result.fileKey}`;
312
-
313
- returnData.push({
314
- json: {
315
- fileKey: result.fileKey,
316
- proxyUrl,
317
- contentType,
318
- fileSize,
319
- },
320
- binary: item.binary,
321
- });
322
- }
323
-
324
- return [returnData];
325
- }
326
-
327
- async function handleDelete(
328
- context: IExecuteFunctions,
329
- items: INodeExecutionData[],
330
- storage: StorageDriver
331
- ): Promise<INodeExecutionData[][]> {
332
- const returnData: INodeExecutionData[] = [];
333
-
334
- for (const item of items) {
335
- const fileKey = (item.json.fileKey || context.getNodeParameter('fileKey', 0)) as string;
336
-
337
- if (!fileKey) {
338
- throw new NodeOperationError(context.getNode(), 'File key is required for delete operation');
339
- }
340
-
341
- await storage.deleteFile(fileKey);
342
-
343
- returnData.push({
344
- json: {
345
- success: true,
346
- deleted: fileKey,
347
- },
348
- });
349
- }
350
-
351
- return [returnData];
352
- }
353
-
354
- function buildWebhookUrl(context: IExecuteFunctions, webhookName: string, path: string): string {
355
- const baseUrl = context.getInstanceBaseUrl();
356
- const node = context.getNode();
357
- const workflow = context.getWorkflow();
358
- const workflowId = workflow.id;
359
- const nodeName = encodeURIComponent(node.name.toLowerCase());
360
- return `${baseUrl}/webhook/${workflowId}/${nodeName}/${path}`;
361
- }
362
-
363
- function isValidFileKey(fileKey: string): boolean {
364
- if (!fileKey || typeof fileKey !== 'string') {
365
- return false;
366
- }
367
-
368
- const fileKeyPattern = /^[0-9]+-[a-z0-9]+\.[a-z0-9]+$/i;
369
- return fileKeyPattern.test(fileKey);
370
- }