n8n-nodes-binary-to-url 0.0.1 → 0.0.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 +388 -55
- package/dist/drivers/S3Storage.d.ts +13 -5
- package/dist/drivers/S3Storage.js +162 -54
- package/dist/drivers/index.d.ts +2 -4
- package/dist/drivers/index.js +1 -17
- package/dist/nodes/BinaryBridge/BinaryBridge.node.js +6 -44
- package/dist/nodes/BinaryBridge/BinaryBridge.node.ts +6 -46
- package/dist/nodes/BinaryBridge/BinaryBridge.svg +96 -4
- package/nodes/BinaryBridge/BinaryBridge.node.ts +6 -46
- package/nodes/BinaryBridge/BinaryBridge.svg +96 -4
- package/package.json +3 -8
- package/dist/drivers/SupabaseStorage.d.ts +0 -26
- package/dist/drivers/SupabaseStorage.js +0 -206
|
@@ -1,86 +1,194 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.S3Storage = void 0;
|
|
4
|
-
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
5
|
-
const stream_1 = require("stream");
|
|
6
4
|
class S3Storage {
|
|
7
5
|
constructor(config) {
|
|
8
|
-
this.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
accessKeyId: config.accessKeyId,
|
|
12
|
-
secretAccessKey: config.secretAccessKey,
|
|
13
|
-
},
|
|
14
|
-
endpoint: config.endpoint,
|
|
15
|
-
forcePathStyle: config.forcePathStyle ?? false,
|
|
16
|
-
});
|
|
17
|
-
this.bucket = config.bucket;
|
|
18
|
-
}
|
|
19
|
-
async uploadStream(stream, contentType, metadata) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
async uploadStream(data, contentType, metadata) {
|
|
20
9
|
const fileKey = this.generateFileKey(contentType);
|
|
10
|
+
const endpoint = this.getEndpoint();
|
|
11
|
+
const url = `${endpoint}/${this.config.bucket}/${fileKey}`;
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': contentType,
|
|
14
|
+
'x-amz-content-sha256': 'UNSIGNED-PAYLOAD',
|
|
15
|
+
};
|
|
16
|
+
if (metadata) {
|
|
17
|
+
Object.entries(metadata).forEach(([key, value]) => {
|
|
18
|
+
headers[`x-amz-meta-${key}`] = value;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const authorization = await this.generateAuthorization('PUT', `/${this.config.bucket}/${fileKey}`, headers);
|
|
21
22
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
method: 'PUT',
|
|
25
|
+
headers: {
|
|
26
|
+
...headers,
|
|
27
|
+
Authorization: authorization,
|
|
28
|
+
},
|
|
29
|
+
body: data,
|
|
28
30
|
});
|
|
29
|
-
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const errorText = await response.text();
|
|
33
|
+
throw new Error(`S3 upload failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
34
|
+
}
|
|
30
35
|
return {
|
|
31
36
|
fileKey,
|
|
32
37
|
contentType,
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
40
|
catch (error) {
|
|
36
|
-
if (error instanceof
|
|
37
|
-
|
|
38
|
-
throw new Error(`S3 bucket "${this.bucket}" does not exist or is not accessible`);
|
|
39
|
-
}
|
|
40
|
-
if (error.name === 'AccessDenied') {
|
|
41
|
-
throw new Error(`Access denied to S3 bucket "${this.bucket}". Check your credentials and bucket permissions`);
|
|
42
|
-
}
|
|
43
|
-
throw new Error(`S3 upload failed: ${error.message}`);
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
throw error;
|
|
44
43
|
}
|
|
45
|
-
throw error;
|
|
44
|
+
throw new Error(`S3 upload failed: ${String(error)}`);
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
async downloadStream(fileKey) {
|
|
48
|
+
const endpoint = this.getEndpoint();
|
|
49
|
+
const url = `${endpoint}/${this.config.bucket}/${fileKey}`;
|
|
50
|
+
const authorization = await this.generateAuthorization('GET', `/${this.config.bucket}/${fileKey}`, {});
|
|
49
51
|
try {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: authorization,
|
|
56
|
+
},
|
|
53
57
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
if (response.status === 404) {
|
|
60
|
+
throw new Error(`File not found: ${fileKey}`);
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 403) {
|
|
63
|
+
throw new Error(`Access denied to bucket "${this.config.bucket}". Check your credentials`);
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`S3 download failed: ${response.status} ${response.statusText}`);
|
|
57
66
|
}
|
|
58
|
-
const
|
|
59
|
-
const
|
|
67
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
68
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
69
|
+
const data = Buffer.from(arrayBuffer);
|
|
60
70
|
return {
|
|
61
|
-
|
|
62
|
-
contentType
|
|
71
|
+
data,
|
|
72
|
+
contentType,
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
catch (error) {
|
|
66
|
-
if (error instanceof
|
|
67
|
-
|
|
68
|
-
throw new Error(`File not found: ${fileKey}`);
|
|
69
|
-
}
|
|
70
|
-
if (error.name === 'AccessDenied') {
|
|
71
|
-
throw new Error(`Access denied to S3 bucket "${this.bucket}". Check your credentials and bucket permissions`);
|
|
72
|
-
}
|
|
73
|
-
throw new Error(`S3 download failed: ${error.message}`);
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
throw error;
|
|
74
78
|
}
|
|
75
|
-
throw error;
|
|
79
|
+
throw new Error(`S3 download failed: ${String(error)}`);
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
async deleteFile(fileKey) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const endpoint = this.getEndpoint();
|
|
84
|
+
const url = `${endpoint}/${this.config.bucket}/${fileKey}`;
|
|
85
|
+
const authorization = await this.generateAuthorization('DELETE', `/${this.config.bucket}/${fileKey}`, {});
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
method: 'DELETE',
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: authorization,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok && response.status !== 204) {
|
|
94
|
+
throw new Error(`S3 delete failed: ${response.status} ${response.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof Error) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`S3 delete failed: ${String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
getEndpoint() {
|
|
105
|
+
if (this.config.endpoint) {
|
|
106
|
+
return this.config.endpoint;
|
|
107
|
+
}
|
|
108
|
+
if (this.config.forcePathStyle) {
|
|
109
|
+
return `https://s3.${this.config.region}.amazonaws.com`;
|
|
110
|
+
}
|
|
111
|
+
return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com`;
|
|
112
|
+
}
|
|
113
|
+
async generateAuthorization(method, path, headers) {
|
|
114
|
+
const now = new Date();
|
|
115
|
+
const amzDate = this.getAmzDate(now);
|
|
116
|
+
const dateStamp = this.getDateStamp(now);
|
|
117
|
+
// Canonical request
|
|
118
|
+
const canonicalHeaders = this.getCanonicalHeaders(headers);
|
|
119
|
+
const signedHeaders = this.getSignedHeaders(headers);
|
|
120
|
+
const payloadHash = 'UNSIGNED-PAYLOAD';
|
|
121
|
+
const canonicalRequest = [
|
|
122
|
+
method,
|
|
123
|
+
path,
|
|
124
|
+
'', // Query string
|
|
125
|
+
canonicalHeaders,
|
|
126
|
+
signedHeaders,
|
|
127
|
+
payloadHash,
|
|
128
|
+
].join('\n');
|
|
129
|
+
const canonicalRequestHash = await this.sha256(canonicalRequest);
|
|
130
|
+
// String to sign
|
|
131
|
+
const credentialScope = `${dateStamp}/${this.config.region}/s3/aws4_request`;
|
|
132
|
+
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, canonicalRequestHash].join('\n');
|
|
133
|
+
// Calculate signature
|
|
134
|
+
const signingKey = await this.getSigningKey(dateStamp);
|
|
135
|
+
const signature = await this.hmac(signingKey, stringToSign);
|
|
136
|
+
// Authorization header
|
|
137
|
+
return `AWS4-HMAC-SHA256 Credential=${this.config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
138
|
+
}
|
|
139
|
+
getAmzDate(date) {
|
|
140
|
+
return date
|
|
141
|
+
.toISOString()
|
|
142
|
+
.replace(/[:-]|.\d{3}/g, '')
|
|
143
|
+
.replace(/T/, 'T');
|
|
144
|
+
}
|
|
145
|
+
getDateStamp(date) {
|
|
146
|
+
return date.toISOString().substring(0, 10).replace(/-/g, '');
|
|
147
|
+
}
|
|
148
|
+
getCanonicalHeaders(headers) {
|
|
149
|
+
const canonicalHeaders = [];
|
|
150
|
+
const lowerCaseHeaders = {};
|
|
151
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
152
|
+
lowerCaseHeaders[key.toLowerCase()] = value.trim();
|
|
153
|
+
}
|
|
154
|
+
for (const [key, value] of Object.entries(lowerCaseHeaders).sort()) {
|
|
155
|
+
canonicalHeaders.push(`${key}:${value}\n`);
|
|
156
|
+
}
|
|
157
|
+
return canonicalHeaders.join('');
|
|
158
|
+
}
|
|
159
|
+
getSignedHeaders(headers) {
|
|
160
|
+
const lowerCaseHeaders = Object.keys(headers).map((h) => h.toLowerCase());
|
|
161
|
+
return lowerCaseHeaders.sort().join(';');
|
|
162
|
+
}
|
|
163
|
+
async sha256(message) {
|
|
164
|
+
const encoder = new TextEncoder();
|
|
165
|
+
const data = encoder.encode(message);
|
|
166
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
167
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
168
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
169
|
+
}
|
|
170
|
+
async hmac(key, message) {
|
|
171
|
+
const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
172
|
+
const encoder = new TextEncoder();
|
|
173
|
+
const data = encoder.encode(message);
|
|
174
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
|
|
175
|
+
const signatureArray = Array.from(new Uint8Array(signature));
|
|
176
|
+
return signatureArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
177
|
+
}
|
|
178
|
+
async getSigningKey(dateStamp) {
|
|
179
|
+
const kDate = await this.hmacSha256(`AWS4${this.config.secretAccessKey}`, dateStamp);
|
|
180
|
+
const kRegion = await this.hmacSha256(kDate, this.config.region);
|
|
181
|
+
const kService = await this.hmacSha256(kRegion, 's3');
|
|
182
|
+
const kSigning = await this.hmacSha256(kService, 'aws4_request');
|
|
183
|
+
return kSigning;
|
|
184
|
+
}
|
|
185
|
+
async hmacSha256(key, message) {
|
|
186
|
+
const keyBuffer = typeof key === 'string' ? Buffer.from(key) : key;
|
|
187
|
+
const encoder = new TextEncoder();
|
|
188
|
+
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);
|
|
191
|
+
return Buffer.from(signature);
|
|
84
192
|
}
|
|
85
193
|
generateFileKey(contentType) {
|
|
86
194
|
const ext = this.getExtensionFromMimeType(contentType);
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { IExecuteFunctions, IWebhookFunctions } from 'n8n-workflow';
|
|
2
2
|
export { S3Storage } from './S3Storage';
|
|
3
3
|
export type { StorageConfig as S3StorageConfig } from './S3Storage';
|
|
4
|
-
export { SupabaseStorage } from './SupabaseStorage';
|
|
5
|
-
export type { SupabaseConfig } from './SupabaseStorage';
|
|
6
4
|
export interface StorageDriver {
|
|
7
|
-
uploadStream(
|
|
5
|
+
uploadStream(data: Buffer, contentType: string, metadata?: Record<string, string>): Promise<{
|
|
8
6
|
fileKey: string;
|
|
9
7
|
contentType: string;
|
|
10
8
|
}>;
|
|
11
9
|
downloadStream(fileKey: string): Promise<{
|
|
12
|
-
|
|
10
|
+
data: Buffer;
|
|
13
11
|
contentType: string;
|
|
14
12
|
}>;
|
|
15
13
|
deleteFile(fileKey: string): Promise<void>;
|
package/dist/drivers/index.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.S3Storage = void 0;
|
|
4
4
|
exports.createStorageDriver = createStorageDriver;
|
|
5
5
|
const S3Storage_1 = require("./S3Storage");
|
|
6
|
-
const SupabaseStorage_1 = require("./SupabaseStorage");
|
|
7
6
|
var S3Storage_2 = require("./S3Storage");
|
|
8
7
|
Object.defineProperty(exports, "S3Storage", { enumerable: true, get: function () { return S3Storage_2.S3Storage; } });
|
|
9
|
-
var SupabaseStorage_2 = require("./SupabaseStorage");
|
|
10
|
-
Object.defineProperty(exports, "SupabaseStorage", { enumerable: true, get: function () { return SupabaseStorage_2.SupabaseStorage; } });
|
|
11
8
|
async function createStorageDriver(context, storageDriver, bucket) {
|
|
12
9
|
if (storageDriver === 's3') {
|
|
13
10
|
const credentials = await context.getCredentials('awsS3Api');
|
|
@@ -27,18 +24,5 @@ async function createStorageDriver(context, storageDriver, bucket) {
|
|
|
27
24
|
};
|
|
28
25
|
return new S3Storage_1.S3Storage(config);
|
|
29
26
|
}
|
|
30
|
-
else if (storageDriver === 'supabase') {
|
|
31
|
-
const credentials = await context.getCredentials('supabaseApi');
|
|
32
|
-
if (!credentials) {
|
|
33
|
-
throw new Error('Supabase credentials are required');
|
|
34
|
-
}
|
|
35
|
-
const projectUrl = context.getNodeParameter('projectUrl', 0);
|
|
36
|
-
const config = {
|
|
37
|
-
projectUrl,
|
|
38
|
-
apiKey: credentials.apiKey,
|
|
39
|
-
bucket,
|
|
40
|
-
};
|
|
41
|
-
return new SupabaseStorage_1.SupabaseStorage(config);
|
|
42
|
-
}
|
|
43
27
|
throw new Error(`Unknown storage driver: ${storageDriver}`);
|
|
44
28
|
}
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BinaryBridge = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
-
const stream_1 = require("stream");
|
|
6
5
|
const drivers_1 = require("../../drivers");
|
|
7
|
-
const file_type_1 = require("file-type");
|
|
8
6
|
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
9
7
|
const ALLOWED_MIME_TYPES = [
|
|
10
8
|
'image/jpeg',
|
|
@@ -44,7 +42,7 @@ class BinaryBridge {
|
|
|
44
42
|
group: ['transform'],
|
|
45
43
|
version: 1,
|
|
46
44
|
subtitle: '={{$parameter["operation"]}}',
|
|
47
|
-
description: 'Upload binary files to storage and proxy them via public URL',
|
|
45
|
+
description: 'Upload binary files to S3 storage and proxy them via public URL',
|
|
48
46
|
defaults: {
|
|
49
47
|
name: 'Binary Bridge',
|
|
50
48
|
},
|
|
@@ -56,11 +54,6 @@ class BinaryBridge {
|
|
|
56
54
|
displayName: 'AWS S3 Credentials',
|
|
57
55
|
required: true,
|
|
58
56
|
},
|
|
59
|
-
{
|
|
60
|
-
name: 'supabaseApi',
|
|
61
|
-
displayName: 'Supabase Credentials',
|
|
62
|
-
required: true,
|
|
63
|
-
},
|
|
64
57
|
],
|
|
65
58
|
webhooks: [
|
|
66
59
|
{
|
|
@@ -83,11 +76,6 @@ class BinaryBridge {
|
|
|
83
76
|
value: 's3',
|
|
84
77
|
description: 'Use AWS S3 or S3-compatible storage (Alibaba OSS, Tencent COS, MinIO, etc.)',
|
|
85
78
|
},
|
|
86
|
-
{
|
|
87
|
-
name: 'Supabase',
|
|
88
|
-
value: 'supabase',
|
|
89
|
-
description: 'Use Supabase Storage',
|
|
90
|
-
},
|
|
91
79
|
],
|
|
92
80
|
default: 's3',
|
|
93
81
|
},
|
|
@@ -181,20 +169,6 @@ class BinaryBridge {
|
|
|
181
169
|
},
|
|
182
170
|
description: 'Use path-style addressing (for MinIO, DigitalOcean Spaces, etc.)',
|
|
183
171
|
},
|
|
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
172
|
],
|
|
199
173
|
};
|
|
200
174
|
}
|
|
@@ -217,7 +191,6 @@ class BinaryBridge {
|
|
|
217
191
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
218
192
|
}
|
|
219
193
|
catch (error) {
|
|
220
|
-
// Enhance error messages with context
|
|
221
194
|
if (error instanceof Error) {
|
|
222
195
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation failed: ${error.message}`);
|
|
223
196
|
}
|
|
@@ -278,11 +251,11 @@ class BinaryBridge {
|
|
|
278
251
|
};
|
|
279
252
|
}
|
|
280
253
|
try {
|
|
281
|
-
const {
|
|
254
|
+
const { data, contentType } = await storage.downloadStream(fileKey);
|
|
282
255
|
return {
|
|
283
256
|
webhookResponse: {
|
|
284
257
|
status: 200,
|
|
285
|
-
body:
|
|
258
|
+
body: data.toString('base64'),
|
|
286
259
|
headers: {
|
|
287
260
|
'Content-Type': contentType,
|
|
288
261
|
'Cache-Control': 'public, max-age=86400',
|
|
@@ -315,18 +288,8 @@ async function handleUpload(context, items, storage) {
|
|
|
315
288
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `No binary data found in property "${binaryPropertyName}"`);
|
|
316
289
|
}
|
|
317
290
|
const buffer = Buffer.from(binaryData.data, 'base64');
|
|
318
|
-
//
|
|
319
|
-
|
|
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
|
-
}
|
|
291
|
+
// Use provided MIME type or default
|
|
292
|
+
const contentType = binaryData.mimeType || 'application/octet-stream';
|
|
330
293
|
if (!ALLOWED_MIME_TYPES.includes(contentType)) {
|
|
331
294
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `MIME type "${contentType}" is not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(', ')}`);
|
|
332
295
|
}
|
|
@@ -334,8 +297,7 @@ async function handleUpload(context, items, storage) {
|
|
|
334
297
|
if (fileSize > MAX_FILE_SIZE) {
|
|
335
298
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `File size exceeds maximum limit of ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
336
299
|
}
|
|
337
|
-
const
|
|
338
|
-
const result = await storage.uploadStream(uploadStream, contentType);
|
|
300
|
+
const result = await storage.uploadStream(buffer, contentType);
|
|
339
301
|
const proxyUrl = `${webhookBaseUrl}/${result.fileKey}`;
|
|
340
302
|
returnData.push({
|
|
341
303
|
json: {
|
|
@@ -7,9 +7,7 @@ import {
|
|
|
7
7
|
INodeExecutionData,
|
|
8
8
|
NodeOperationError,
|
|
9
9
|
} from 'n8n-workflow';
|
|
10
|
-
import { Readable } from 'stream';
|
|
11
10
|
import { createStorageDriver, StorageDriver } from '../../drivers';
|
|
12
|
-
import { fileTypeFromBuffer } from 'file-type';
|
|
13
11
|
|
|
14
12
|
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
15
13
|
const ALLOWED_MIME_TYPES = [
|
|
@@ -50,7 +48,7 @@ export class BinaryBridge implements INodeType {
|
|
|
50
48
|
group: ['transform'],
|
|
51
49
|
version: 1,
|
|
52
50
|
subtitle: '={{$parameter["operation"]}}',
|
|
53
|
-
description: 'Upload binary files to storage and proxy them via public URL',
|
|
51
|
+
description: 'Upload binary files to S3 storage and proxy them via public URL',
|
|
54
52
|
defaults: {
|
|
55
53
|
name: 'Binary Bridge',
|
|
56
54
|
},
|
|
@@ -62,11 +60,6 @@ export class BinaryBridge implements INodeType {
|
|
|
62
60
|
displayName: 'AWS S3 Credentials',
|
|
63
61
|
required: true,
|
|
64
62
|
},
|
|
65
|
-
{
|
|
66
|
-
name: 'supabaseApi',
|
|
67
|
-
displayName: 'Supabase Credentials',
|
|
68
|
-
required: true,
|
|
69
|
-
},
|
|
70
63
|
],
|
|
71
64
|
webhooks: [
|
|
72
65
|
{
|
|
@@ -90,11 +83,6 @@ export class BinaryBridge implements INodeType {
|
|
|
90
83
|
description:
|
|
91
84
|
'Use AWS S3 or S3-compatible storage (Alibaba OSS, Tencent COS, MinIO, etc.)',
|
|
92
85
|
},
|
|
93
|
-
{
|
|
94
|
-
name: 'Supabase',
|
|
95
|
-
value: 'supabase',
|
|
96
|
-
description: 'Use Supabase Storage',
|
|
97
|
-
},
|
|
98
86
|
],
|
|
99
87
|
default: 's3',
|
|
100
88
|
},
|
|
@@ -189,20 +177,6 @@ export class BinaryBridge implements INodeType {
|
|
|
189
177
|
},
|
|
190
178
|
description: 'Use path-style addressing (for MinIO, DigitalOcean Spaces, etc.)',
|
|
191
179
|
},
|
|
192
|
-
{
|
|
193
|
-
displayName: 'Project URL',
|
|
194
|
-
name: 'projectUrl',
|
|
195
|
-
type: 'string',
|
|
196
|
-
default: '',
|
|
197
|
-
required: true,
|
|
198
|
-
displayOptions: {
|
|
199
|
-
show: {
|
|
200
|
-
storageDriver: ['supabase'],
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
placeholder: 'https://your-project.supabase.co',
|
|
204
|
-
description: 'Supabase project URL',
|
|
205
|
-
},
|
|
206
180
|
],
|
|
207
181
|
};
|
|
208
182
|
|
|
@@ -227,7 +201,6 @@ export class BinaryBridge implements INodeType {
|
|
|
227
201
|
|
|
228
202
|
throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
229
203
|
} catch (error) {
|
|
230
|
-
// Enhance error messages with context
|
|
231
204
|
if (error instanceof Error) {
|
|
232
205
|
throw new NodeOperationError(this.getNode(), `Operation failed: ${error.message}`);
|
|
233
206
|
}
|
|
@@ -294,12 +267,12 @@ export class BinaryBridge implements INodeType {
|
|
|
294
267
|
}
|
|
295
268
|
|
|
296
269
|
try {
|
|
297
|
-
const {
|
|
270
|
+
const { data, contentType } = await storage.downloadStream(fileKey);
|
|
298
271
|
|
|
299
272
|
return {
|
|
300
273
|
webhookResponse: {
|
|
301
274
|
status: 200,
|
|
302
|
-
body:
|
|
275
|
+
body: data.toString('base64'),
|
|
303
276
|
headers: {
|
|
304
277
|
'Content-Type': contentType,
|
|
305
278
|
'Cache-Control': 'public, max-age=86400',
|
|
@@ -343,19 +316,8 @@ async function handleUpload(
|
|
|
343
316
|
|
|
344
317
|
const buffer = Buffer.from(binaryData.data, 'base64');
|
|
345
318
|
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const detection = await fileTypeFromBuffer(buffer);
|
|
350
|
-
if (detection) {
|
|
351
|
-
contentType = detection.mime;
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
// Fall back to provided MIME type or default
|
|
355
|
-
console.warn(
|
|
356
|
-
`Failed to detect MIME type: ${error instanceof Error ? error.message : String(error)}`
|
|
357
|
-
);
|
|
358
|
-
}
|
|
319
|
+
// Use provided MIME type or default
|
|
320
|
+
const contentType = binaryData.mimeType || 'application/octet-stream';
|
|
359
321
|
|
|
360
322
|
if (!ALLOWED_MIME_TYPES.includes(contentType)) {
|
|
361
323
|
throw new NodeOperationError(
|
|
@@ -372,9 +334,7 @@ async function handleUpload(
|
|
|
372
334
|
);
|
|
373
335
|
}
|
|
374
336
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
const result = await storage.uploadStream(uploadStream, contentType);
|
|
337
|
+
const result = await storage.uploadStream(buffer, contentType);
|
|
378
338
|
|
|
379
339
|
const proxyUrl = `${webhookBaseUrl}/${result.fileKey}`;
|
|
380
340
|
|
|
@@ -1,5 +1,97 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<!-- Gradient for the cloud -->
|
|
4
|
+
<linearGradient id="cloudGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
5
|
+
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
|
6
|
+
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
|
7
|
+
</linearGradient>
|
|
8
|
+
|
|
9
|
+
<!-- Gradient for the file -->
|
|
10
|
+
<linearGradient id="fileGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
11
|
+
<stop offset="0%" style="stop-color:#60A5FA;stop-opacity:1" />
|
|
12
|
+
<stop offset="100%" style="stop-color:#3B82F6;stop-opacity:1" />
|
|
13
|
+
</linearGradient>
|
|
14
|
+
|
|
15
|
+
<!-- Gradient for the bridge/connection -->
|
|
16
|
+
<linearGradient id="bridgeGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
17
|
+
<stop offset="0%" style="stop-color:#34D399;stop-opacity:1" />
|
|
18
|
+
<stop offset="100%" style="stop-color:#10B981;stop-opacity:1" />
|
|
19
|
+
</linearGradient>
|
|
20
|
+
</defs>
|
|
21
|
+
|
|
22
|
+
<!-- Background circle with subtle gradient -->
|
|
23
|
+
<circle cx="32" cy="32" r="30" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="2"/>
|
|
24
|
+
|
|
25
|
+
<!-- Cloud Storage Icon -->
|
|
26
|
+
<g transform="translate(16, 12)">
|
|
27
|
+
<!-- Cloud shape -->
|
|
28
|
+
<path d="M6 14
|
|
29
|
+
C2 14 0 12 0 9
|
|
30
|
+
C0 6 2 4 5 4
|
|
31
|
+
C5.5 1.5 8 0 11 0
|
|
32
|
+
C14.5 0 17.5 2 18.5 5
|
|
33
|
+
C21 5 23 7 23 9.5
|
|
34
|
+
C23 12 21 14 18 14
|
|
35
|
+
Z"
|
|
36
|
+
fill="url(#cloudGradient)"
|
|
37
|
+
opacity="0.9"/>
|
|
38
|
+
|
|
39
|
+
<!-- S3/Storage lines -->
|
|
40
|
+
<path d="M4 9 C4 9 8 11 11.5 11 C15 11 19 9 19 9"
|
|
41
|
+
stroke="white"
|
|
42
|
+
stroke-width="1.5"
|
|
43
|
+
stroke-linecap="round"
|
|
44
|
+
opacity="0.6"/>
|
|
45
|
+
<path d="M4 11 C4 11 8 13 11.5 13 C15 13 19 11 19 11"
|
|
46
|
+
stroke="white"
|
|
47
|
+
stroke-width="1.5"
|
|
48
|
+
stroke-linecap="round"
|
|
49
|
+
opacity="0.6"/>
|
|
50
|
+
</g>
|
|
51
|
+
|
|
52
|
+
<!-- Binary File Icon -->
|
|
53
|
+
<g transform="translate(22, 26)">
|
|
54
|
+
<!-- File background -->
|
|
55
|
+
<rect x="0" y="0" width="20" height="26" rx="2" fill="url(#fileGradient)"/>
|
|
56
|
+
|
|
57
|
+
<!-- File corner fold -->
|
|
58
|
+
<path d="M14 0 L20 6 L14 6 Z" fill="#2563EB" opacity="0.5"/>
|
|
59
|
+
|
|
60
|
+
<!-- Binary code representation -->
|
|
61
|
+
<g fill="white" opacity="0.9">
|
|
62
|
+
<rect x="4" y="10" width="2" height="2" rx="0.5"/>
|
|
63
|
+
<rect x="8" y="10" width="2" height="2" rx="0.5"/>
|
|
64
|
+
<rect x="4" y="14" width="2" height="2" rx="0.5"/>
|
|
65
|
+
<rect x="8" y="14" width="2" height="2" rx="0.5"/>
|
|
66
|
+
<rect x="10" y="10" width="2" height="2" rx="0.5"/>
|
|
67
|
+
<rect x="14" y="10" width="2" height="2" rx="0.5"/>
|
|
68
|
+
<rect x="10" y="14" width="2" height="2" rx="0.5"/>
|
|
69
|
+
<rect x="14" y="14" width="2" height="2" rx="0.5"/>
|
|
70
|
+
|
|
71
|
+
<!-- Bottom row -->
|
|
72
|
+
<rect x="4" y="18" width="12" height="2" rx="1"/>
|
|
73
|
+
</g>
|
|
74
|
+
</g>
|
|
75
|
+
|
|
76
|
+
<!-- Connection/Bridge arrows -->
|
|
77
|
+
<g transform="translate(6, 38)">
|
|
78
|
+
<!-- Arrow pointing up (upload) -->
|
|
79
|
+
<path d="M8 12 L8 4 M4 8 L8 4 L12 8"
|
|
80
|
+
stroke="url(#bridgeGradient)"
|
|
81
|
+
stroke-width="2.5"
|
|
82
|
+
stroke-linecap="round"
|
|
83
|
+
stroke-linejoin="round"
|
|
84
|
+
fill="none"/>
|
|
85
|
+
|
|
86
|
+
<!-- Arrow pointing down (download) -->
|
|
87
|
+
<path d="M20 4 L20 12 M16 8 L20 12 L24 8"
|
|
88
|
+
stroke="url(#bridgeGradient)"
|
|
89
|
+
stroke-width="2.5"
|
|
90
|
+
stroke-linecap="round"
|
|
91
|
+
stroke-linejoin="round"
|
|
92
|
+
fill="none"/>
|
|
93
|
+
|
|
94
|
+
<!-- Connection dots -->
|
|
95
|
+
<circle cx="14" cy="8" r="2" fill="#10B981" opacity="0.6"/>
|
|
96
|
+
</g>
|
|
5
97
|
</svg>
|