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.
@@ -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.s3Client = new client_s3_1.S3Client({
9
- region: config.region,
10
- credentials: {
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 command = new client_s3_1.PutObjectCommand({
23
- Bucket: this.bucket,
24
- Key: fileKey,
25
- Body: stream,
26
- ContentType: contentType,
27
- Metadata: metadata || {},
23
+ const response = await fetch(url, {
24
+ method: 'PUT',
25
+ headers: {
26
+ ...headers,
27
+ Authorization: authorization,
28
+ },
29
+ body: data,
28
30
  });
29
- await this.s3Client.send(command);
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 client_s3_1.S3ServiceException) {
37
- if (error.name === 'NoSuchBucket') {
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 command = new client_s3_1.GetObjectCommand({
51
- Bucket: this.bucket,
52
- Key: fileKey,
52
+ const response = await fetch(url, {
53
+ method: 'GET',
54
+ headers: {
55
+ Authorization: authorization,
56
+ },
53
57
  });
54
- const response = await this.s3Client.send(command);
55
- if (!response.Body) {
56
- throw new Error(`File not found: ${fileKey}`);
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 body = response.Body;
59
- const stream = body instanceof stream_1.Readable ? body : stream_1.Readable.from(response.Body);
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
- stream,
62
- contentType: response.ContentType || 'application/octet-stream',
71
+ data,
72
+ contentType,
63
73
  };
64
74
  }
65
75
  catch (error) {
66
- if (error instanceof client_s3_1.S3ServiceException) {
67
- if (error.name === 'NoSuchKey' || error.name === 'NotFound') {
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 command = new client_s3_1.DeleteObjectCommand({
80
- Bucket: this.bucket,
81
- Key: fileKey,
82
- });
83
- await this.s3Client.send(command);
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);
@@ -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(stream: any, contentType: string, metadata?: Record<string, string>): Promise<{
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
- stream: any;
10
+ data: Buffer;
13
11
  contentType: string;
14
12
  }>;
15
13
  deleteFile(fileKey: string): Promise<void>;
@@ -1,13 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SupabaseStorage = exports.S3Storage = void 0;
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 { stream, contentType } = await storage.downloadStream(fileKey);
254
+ const { data, contentType } = await storage.downloadStream(fileKey);
282
255
  return {
283
256
  webhookResponse: {
284
257
  status: 200,
285
- body: stream,
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
- // 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
- }
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 uploadStream = stream_1.Readable.from(buffer);
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 { stream, contentType } = await storage.downloadStream(fileKey);
270
+ const { data, contentType } = await storage.downloadStream(fileKey);
298
271
 
299
272
  return {
300
273
  webhookResponse: {
301
274
  status: 200,
302
- body: stream,
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
- // Try to detect MIME type from file signature using file-type
347
- let contentType = binaryData.mimeType || 'application/octet-stream';
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 uploadStream = Readable.from(buffer);
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 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
3
- <polyline points="17 8 12 3 7 8" />
4
- <line x1="12" y1="3" x2="12" y2="15" />
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>