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.
- package/dist/credentials/S3StorageApi.credentials.d.ts +15 -0
- package/dist/credentials/S3StorageApi.credentials.js +35 -0
- package/dist/drivers/S3Storage.js +71 -5
- package/dist/drivers/index.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/nodes/{BinaryBridge/BinaryBridge.node.d.ts → BinaryToUrl/BinaryToUrl.node.d.ts} +1 -1
- package/dist/nodes/{BinaryBridge/BinaryBridge.node.js → BinaryToUrl/BinaryToUrl.node.js} +7 -6
- package/package.json +18 -20
- package/dist/nodes/BinaryBridge/BinaryBridge.node.ts +0 -370
- package/nodes/BinaryBridge/BinaryBridge.node.ts +0 -370
- /package/dist/{nodes/BinaryBridge/BinaryBridge.svg → icons/BinaryToUrl.svg} +0 -0
- /package/{nodes/BinaryBridge → dist/nodes/BinaryToUrl}/BinaryBridge.svg +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
190
|
-
const signature = await
|
|
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) {
|
package/dist/drivers/index.js
CHANGED
|
@@ -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('
|
|
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 {
|
|
2
|
-
export declare const nodeClasses: (typeof
|
|
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
|
|
5
|
-
exports.nodeClasses = [
|
|
4
|
+
const BinaryToUrl_node_1 = require("./nodes/BinaryToUrl/BinaryToUrl.node");
|
|
5
|
+
exports.nodeClasses = [BinaryToUrl_node_1.BinaryToUrl];
|
package/dist/nodes/{BinaryBridge/BinaryBridge.node.d.ts → BinaryToUrl/BinaryToUrl.node.d.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { INodeType, INodeTypeDescription, IExecuteFunctions, IWebhookFunctions, IWebhookResponseData, INodeExecutionData } from 'n8n-workflow';
|
|
2
|
-
export declare class
|
|
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.
|
|
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
|
|
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: '
|
|
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: '
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
28
|
-
"
|
|
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": "
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
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
|
-
"
|
|
41
|
+
"strict": true,
|
|
42
|
+
"credentials": [
|
|
43
|
+
"dist/credentials/S3StorageApi.credentials.js"
|
|
44
|
+
],
|
|
42
45
|
"nodes": [
|
|
43
|
-
"dist/nodes/
|
|
46
|
+
"dist/nodes/BinaryToUrl/BinaryToUrl.node.js"
|
|
44
47
|
]
|
|
45
48
|
},
|
|
46
49
|
"dependencies": {},
|
|
47
50
|
"devDependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"
|
|
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
|
-
"
|
|
55
|
-
"
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|