n8n-nodes-binary-to-url 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BinaryBridge = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const stream_1 = require("stream");
6
+ const drivers_1 = require("../../drivers");
7
+ const file_type_1 = require("file-type");
8
+ const MAX_FILE_SIZE = 100 * 1024 * 1024;
9
+ const ALLOWED_MIME_TYPES = [
10
+ 'image/jpeg',
11
+ 'image/png',
12
+ 'image/gif',
13
+ 'image/webp',
14
+ 'image/svg+xml',
15
+ 'image/bmp',
16
+ 'image/tiff',
17
+ 'image/avif',
18
+ 'video/mp4',
19
+ 'video/webm',
20
+ 'video/quicktime',
21
+ 'video/x-msvideo',
22
+ 'video/x-matroska',
23
+ 'application/pdf',
24
+ 'application/zip',
25
+ 'application/x-rar-compressed',
26
+ 'application/x-7z-compressed',
27
+ 'audio/mpeg',
28
+ 'audio/wav',
29
+ 'audio/ogg',
30
+ 'audio/flac',
31
+ 'text/plain',
32
+ 'text/csv',
33
+ 'application/json',
34
+ 'application/xml',
35
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
36
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
37
+ ];
38
+ class BinaryBridge {
39
+ constructor() {
40
+ this.description = {
41
+ displayName: 'Binary Bridge',
42
+ name: 'binaryBridge',
43
+ icon: 'file:BinaryBridge.svg',
44
+ group: ['transform'],
45
+ version: 1,
46
+ subtitle: '={{$parameter["operation"]}}',
47
+ description: 'Upload binary files to storage and proxy them via public URL',
48
+ defaults: {
49
+ name: 'Binary Bridge',
50
+ },
51
+ inputs: ['main'],
52
+ outputs: ['main'],
53
+ credentials: [
54
+ {
55
+ name: 'awsS3Api',
56
+ displayName: 'AWS S3 Credentials',
57
+ required: true,
58
+ },
59
+ {
60
+ name: 'supabaseApi',
61
+ displayName: 'Supabase Credentials',
62
+ required: true,
63
+ },
64
+ ],
65
+ webhooks: [
66
+ {
67
+ name: 'default',
68
+ httpMethod: 'GET',
69
+ responseMode: 'onReceived',
70
+ path: 'file/:fileKey',
71
+ isFullPath: true,
72
+ },
73
+ ],
74
+ properties: [
75
+ {
76
+ displayName: 'Storage Driver',
77
+ name: 'storageDriver',
78
+ type: 'options',
79
+ noDataExpression: true,
80
+ options: [
81
+ {
82
+ name: 'AWS S3',
83
+ value: 's3',
84
+ description: 'Use AWS S3 or S3-compatible storage (Alibaba OSS, Tencent COS, MinIO, etc.)',
85
+ },
86
+ {
87
+ name: 'Supabase',
88
+ value: 'supabase',
89
+ description: 'Use Supabase Storage',
90
+ },
91
+ ],
92
+ default: 's3',
93
+ },
94
+ {
95
+ displayName: 'Operation',
96
+ name: 'operation',
97
+ type: 'options',
98
+ noDataExpression: true,
99
+ options: [
100
+ {
101
+ name: 'Upload',
102
+ value: 'upload',
103
+ description: 'Upload binary file to storage',
104
+ action: 'Upload file',
105
+ },
106
+ {
107
+ name: 'Delete',
108
+ value: 'delete',
109
+ description: 'Delete file from storage',
110
+ action: 'Delete file',
111
+ },
112
+ ],
113
+ default: 'upload',
114
+ },
115
+ {
116
+ displayName: 'Binary Property',
117
+ name: 'binaryPropertyName',
118
+ type: 'string',
119
+ displayOptions: {
120
+ show: {
121
+ operation: ['upload'],
122
+ },
123
+ },
124
+ default: 'data',
125
+ description: 'Name of binary property containing the file to upload',
126
+ },
127
+ {
128
+ displayName: 'File Key',
129
+ name: 'fileKey',
130
+ type: 'string',
131
+ displayOptions: {
132
+ show: {
133
+ operation: ['delete'],
134
+ },
135
+ },
136
+ default: '',
137
+ description: 'Key of the file to delete from storage',
138
+ },
139
+ {
140
+ displayName: 'Bucket',
141
+ name: 'bucket',
142
+ type: 'string',
143
+ default: '',
144
+ required: true,
145
+ description: 'Storage bucket name',
146
+ },
147
+ {
148
+ displayName: 'Region',
149
+ name: 'region',
150
+ type: 'string',
151
+ default: 'us-east-1',
152
+ required: true,
153
+ displayOptions: {
154
+ show: {
155
+ storageDriver: ['s3'],
156
+ },
157
+ },
158
+ description: 'AWS region',
159
+ },
160
+ {
161
+ displayName: 'Custom Endpoint',
162
+ name: 'endpoint',
163
+ type: 'string',
164
+ default: '',
165
+ displayOptions: {
166
+ show: {
167
+ storageDriver: ['s3'],
168
+ },
169
+ },
170
+ description: 'Custom S3 endpoint URL (for S3-compatible services like Alibaba OSS, Tencent COS, MinIO, etc.)',
171
+ },
172
+ {
173
+ displayName: 'Force Path Style',
174
+ name: 'forcePathStyle',
175
+ type: 'boolean',
176
+ default: false,
177
+ displayOptions: {
178
+ show: {
179
+ storageDriver: ['s3'],
180
+ },
181
+ },
182
+ description: 'Use path-style addressing (for MinIO, DigitalOcean Spaces, etc.)',
183
+ },
184
+ {
185
+ displayName: 'Project URL',
186
+ name: 'projectUrl',
187
+ type: 'string',
188
+ default: '',
189
+ required: true,
190
+ displayOptions: {
191
+ show: {
192
+ storageDriver: ['supabase'],
193
+ },
194
+ },
195
+ placeholder: 'https://your-project.supabase.co',
196
+ description: 'Supabase project URL',
197
+ },
198
+ ],
199
+ };
200
+ }
201
+ async execute() {
202
+ const items = this.getInputData();
203
+ const operation = this.getNodeParameter('operation', 0);
204
+ const storageDriver = this.getNodeParameter('storageDriver', 0);
205
+ const bucket = this.getNodeParameter('bucket', 0);
206
+ if (!bucket) {
207
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Bucket name is required');
208
+ }
209
+ try {
210
+ const storage = await (0, drivers_1.createStorageDriver)(this, storageDriver, bucket);
211
+ if (operation === 'upload') {
212
+ return handleUpload(this, items, storage);
213
+ }
214
+ else if (operation === 'delete') {
215
+ return handleDelete(this, items, storage);
216
+ }
217
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
218
+ }
219
+ catch (error) {
220
+ // Enhance error messages with context
221
+ if (error instanceof Error) {
222
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation failed: ${error.message}`);
223
+ }
224
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation failed: ${String(error)}`);
225
+ }
226
+ }
227
+ async webhook() {
228
+ const req = this.getRequestObject();
229
+ const fileKey = req.params.fileKey;
230
+ if (!fileKey) {
231
+ return {
232
+ webhookResponse: {
233
+ status: 400,
234
+ body: JSON.stringify({ error: 'Missing fileKey' }),
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ },
238
+ },
239
+ };
240
+ }
241
+ if (!isValidFileKey(fileKey)) {
242
+ return {
243
+ webhookResponse: {
244
+ status: 400,
245
+ body: JSON.stringify({ error: 'Invalid fileKey' }),
246
+ headers: {
247
+ 'Content-Type': 'application/json',
248
+ },
249
+ },
250
+ };
251
+ }
252
+ const bucket = this.getNodeParameter('bucket', 0);
253
+ if (!bucket) {
254
+ return {
255
+ webhookResponse: {
256
+ status: 500,
257
+ body: JSON.stringify({ error: 'Node configuration is incomplete' }),
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ },
261
+ },
262
+ };
263
+ }
264
+ const storageDriver = this.getNodeParameter('storageDriver', 0);
265
+ let storage;
266
+ try {
267
+ storage = await (0, drivers_1.createStorageDriver)(this, storageDriver, bucket);
268
+ }
269
+ catch (error) {
270
+ return {
271
+ webhookResponse: {
272
+ status: 500,
273
+ body: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }),
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ },
277
+ },
278
+ };
279
+ }
280
+ try {
281
+ const { stream, contentType } = await storage.downloadStream(fileKey);
282
+ return {
283
+ webhookResponse: {
284
+ status: 200,
285
+ body: stream,
286
+ headers: {
287
+ 'Content-Type': contentType,
288
+ 'Cache-Control': 'public, max-age=86400',
289
+ 'Content-Disposition': 'inline',
290
+ },
291
+ },
292
+ };
293
+ }
294
+ catch (error) {
295
+ return {
296
+ webhookResponse: {
297
+ status: 404,
298
+ body: JSON.stringify({ error: 'File not found' }),
299
+ headers: {
300
+ 'Content-Type': 'application/json',
301
+ },
302
+ },
303
+ };
304
+ }
305
+ }
306
+ }
307
+ exports.BinaryBridge = BinaryBridge;
308
+ async function handleUpload(context, items, storage) {
309
+ const binaryPropertyName = context.getNodeParameter('binaryPropertyName', 0);
310
+ const webhookBaseUrl = buildWebhookUrl(context, 'default', 'file');
311
+ const returnData = [];
312
+ for (const item of items) {
313
+ const binaryData = item.binary?.[binaryPropertyName];
314
+ if (!binaryData) {
315
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `No binary data found in property "${binaryPropertyName}"`);
316
+ }
317
+ const buffer = Buffer.from(binaryData.data, 'base64');
318
+ // Try to detect MIME type from file signature using file-type
319
+ let contentType = binaryData.mimeType || 'application/octet-stream';
320
+ try {
321
+ const detection = await (0, file_type_1.fileTypeFromBuffer)(buffer);
322
+ if (detection) {
323
+ contentType = detection.mime;
324
+ }
325
+ }
326
+ catch (error) {
327
+ // Fall back to provided MIME type or default
328
+ console.warn(`Failed to detect MIME type: ${error instanceof Error ? error.message : String(error)}`);
329
+ }
330
+ if (!ALLOWED_MIME_TYPES.includes(contentType)) {
331
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `MIME type "${contentType}" is not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}`);
332
+ }
333
+ const fileSize = buffer.length;
334
+ if (fileSize > MAX_FILE_SIZE) {
335
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `File size exceeds maximum limit of ${MAX_FILE_SIZE / 1024 / 1024}MB`);
336
+ }
337
+ const uploadStream = stream_1.Readable.from(buffer);
338
+ const result = await storage.uploadStream(uploadStream, contentType);
339
+ const proxyUrl = `${webhookBaseUrl}/${result.fileKey}`;
340
+ returnData.push({
341
+ json: {
342
+ fileKey: result.fileKey,
343
+ proxyUrl,
344
+ contentType,
345
+ fileSize,
346
+ },
347
+ binary: item.binary,
348
+ });
349
+ }
350
+ return [returnData];
351
+ }
352
+ async function handleDelete(context, items, storage) {
353
+ const returnData = [];
354
+ for (const item of items) {
355
+ const fileKey = (item.json.fileKey || context.getNodeParameter('fileKey', 0));
356
+ if (!fileKey) {
357
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'File key is required for delete operation');
358
+ }
359
+ await storage.deleteFile(fileKey);
360
+ returnData.push({
361
+ json: {
362
+ success: true,
363
+ deleted: fileKey,
364
+ },
365
+ });
366
+ }
367
+ return [returnData];
368
+ }
369
+ function buildWebhookUrl(context, webhookName, path) {
370
+ const baseUrl = context.getInstanceBaseUrl();
371
+ const node = context.getNode();
372
+ const workflow = context.getWorkflow();
373
+ const workflowId = workflow.id;
374
+ const nodeName = encodeURIComponent(node.name.toLowerCase());
375
+ return `${baseUrl}/webhook/${workflowId}/${nodeName}/${path}`;
376
+ }
377
+ function isValidFileKey(fileKey) {
378
+ if (!fileKey || typeof fileKey !== 'string') {
379
+ return false;
380
+ }
381
+ const fileKeyPattern = /^[0-9]+-[a-z0-9]+\.[a-z0-9]+$/i;
382
+ return fileKeyPattern.test(fileKey);
383
+ }