@mastra/s3 0.6.0 → 0.6.1-alpha.0
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/CHANGELOG.md +9 -0
- package/dist/index.cjs +863 -831
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +860 -827
- package/dist/index.js.map +1 -1
- package/package.json +15 -14
package/dist/index.js
CHANGED
|
@@ -1,844 +1,877 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { BlobStore } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
".zip": "application/zip",
|
|
41
|
-
".gz": "application/gzip",
|
|
42
|
-
".tar": "application/x-tar"
|
|
1
|
+
import { CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { FileExistsError, FileNotFoundError, MastraFilesystem } from "@mastra/core/workspace";
|
|
3
|
+
import { BlobStore } from "@mastra/core/storage";
|
|
4
|
+
//#region src/filesystem/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Common MIME types by file extension.
|
|
7
|
+
*/
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
".txt": "text/plain",
|
|
10
|
+
".md": "text/markdown",
|
|
11
|
+
".markdown": "text/markdown",
|
|
12
|
+
".html": "text/html",
|
|
13
|
+
".htm": "text/html",
|
|
14
|
+
".css": "text/css",
|
|
15
|
+
".csv": "text/csv",
|
|
16
|
+
".xml": "text/xml",
|
|
17
|
+
".js": "text/javascript",
|
|
18
|
+
".mjs": "text/javascript",
|
|
19
|
+
".ts": "text/typescript",
|
|
20
|
+
".tsx": "text/typescript",
|
|
21
|
+
".jsx": "text/javascript",
|
|
22
|
+
".json": "application/json",
|
|
23
|
+
".yaml": "text/yaml",
|
|
24
|
+
".yml": "text/yaml",
|
|
25
|
+
".py": "text/x-python",
|
|
26
|
+
".rb": "text/x-ruby",
|
|
27
|
+
".sh": "text/x-shellscript",
|
|
28
|
+
".bash": "text/x-shellscript",
|
|
29
|
+
".png": "image/png",
|
|
30
|
+
".jpg": "image/jpeg",
|
|
31
|
+
".jpeg": "image/jpeg",
|
|
32
|
+
".gif": "image/gif",
|
|
33
|
+
".svg": "image/svg+xml",
|
|
34
|
+
".webp": "image/webp",
|
|
35
|
+
".ico": "image/x-icon",
|
|
36
|
+
".pdf": "application/pdf",
|
|
37
|
+
".zip": "application/zip",
|
|
38
|
+
".gz": "application/gzip",
|
|
39
|
+
".tar": "application/x-tar"
|
|
43
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Get MIME type from file path extension.
|
|
43
|
+
*/
|
|
44
44
|
function getMimeType(path) {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
|
|
46
|
+
return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
/** Check if an error is a "not found" error from the S3 SDK. */
|
|
49
|
+
function isNotFoundError$1(error) {
|
|
50
|
+
if (!error || typeof error !== "object" || !("name" in error)) return false;
|
|
51
|
+
const name = error.name;
|
|
52
|
+
return name === "NotFound" || name === "NoSuchKey" || name === "404";
|
|
52
53
|
}
|
|
54
|
+
/** Check if an error is an access denied error from the S3 SDK. */
|
|
53
55
|
function isAccessDeniedError(error) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (!error || typeof error !== "object") return false;
|
|
57
|
+
const err = error;
|
|
58
|
+
return err.name === "AccessDenied" || err.$metadata?.httpStatusCode === 403;
|
|
57
59
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
/**
|
|
61
|
+
* S3 filesystem implementation.
|
|
62
|
+
*
|
|
63
|
+
* Stores files in an S3 bucket or S3-compatible storage service.
|
|
64
|
+
* Supports mounting into E2B sandboxes via s3fs-fuse.
|
|
65
|
+
*
|
|
66
|
+
* @example AWS S3
|
|
67
|
+
* ```typescript
|
|
68
|
+
* import { S3Filesystem } from '@mastra/s3';
|
|
69
|
+
*
|
|
70
|
+
* const fs = new S3Filesystem({
|
|
71
|
+
* bucket: 'my-bucket',
|
|
72
|
+
* region: 'us-east-1',
|
|
73
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
74
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example Cloudflare R2
|
|
79
|
+
* ```typescript
|
|
80
|
+
* import { S3Filesystem } from '@mastra/s3';
|
|
81
|
+
*
|
|
82
|
+
* const fs = new S3Filesystem({
|
|
83
|
+
* bucket: 'my-bucket',
|
|
84
|
+
* region: 'auto',
|
|
85
|
+
* accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
86
|
+
* secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
87
|
+
* endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example MinIO (local)
|
|
92
|
+
* ```typescript
|
|
93
|
+
* import { S3Filesystem } from '@mastra/s3';
|
|
94
|
+
*
|
|
95
|
+
* const fs = new S3Filesystem({
|
|
96
|
+
* bucket: 'my-bucket',
|
|
97
|
+
* region: 'us-east-1',
|
|
98
|
+
* accessKeyId: 'minioadmin',
|
|
99
|
+
* secretAccessKey: 'minioadmin',
|
|
100
|
+
* endpoint: 'http://localhost:9000',
|
|
101
|
+
* forcePathStyle: true,
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
/** Trim leading and trailing slashes without regex (avoids polynomial regex on user input). */
|
|
106
|
+
function trimSlashes$1(s) {
|
|
107
|
+
let start = 0;
|
|
108
|
+
let end = s.length;
|
|
109
|
+
while (start < end && s[start] === "/") start++;
|
|
110
|
+
while (end > start && s[end - 1] === "/") end--;
|
|
111
|
+
return s.slice(start, end);
|
|
64
112
|
}
|
|
65
113
|
var S3Filesystem = class extends MastraFilesystem {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
const response = await client.send(
|
|
527
|
-
new HeadObjectCommand({
|
|
528
|
-
Bucket: this.bucket,
|
|
529
|
-
Key: key
|
|
530
|
-
})
|
|
531
|
-
);
|
|
532
|
-
const name = path.split("/").pop() ?? "";
|
|
533
|
-
return {
|
|
534
|
-
name,
|
|
535
|
-
path,
|
|
536
|
-
type: "file",
|
|
537
|
-
size: response.ContentLength ?? 0,
|
|
538
|
-
createdAt: response.LastModified ?? /* @__PURE__ */ new Date(),
|
|
539
|
-
modifiedAt: response.LastModified ?? /* @__PURE__ */ new Date()
|
|
540
|
-
};
|
|
541
|
-
} catch (error) {
|
|
542
|
-
if (!isNotFoundError(error)) throw this.handleError(error);
|
|
543
|
-
const isDir = await this.isDirectory(path);
|
|
544
|
-
if (isDir) {
|
|
545
|
-
const name = path.split("/").filter(Boolean).pop() ?? "";
|
|
546
|
-
return {
|
|
547
|
-
name,
|
|
548
|
-
path,
|
|
549
|
-
type: "directory",
|
|
550
|
-
size: 0,
|
|
551
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
552
|
-
modifiedAt: /* @__PURE__ */ new Date()
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
throw new FileNotFoundError(path);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
async isFile(path) {
|
|
559
|
-
const key = this.toKey(path);
|
|
560
|
-
if (!key) return false;
|
|
561
|
-
const client = await this.getReadyClient();
|
|
562
|
-
try {
|
|
563
|
-
await client.send(
|
|
564
|
-
new HeadObjectCommand({
|
|
565
|
-
Bucket: this.bucket,
|
|
566
|
-
Key: key
|
|
567
|
-
})
|
|
568
|
-
);
|
|
569
|
-
return true;
|
|
570
|
-
} catch (error) {
|
|
571
|
-
if (!isNotFoundError(error)) throw this.handleError(error);
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
async isDirectory(path) {
|
|
576
|
-
const key = this.toKey(path);
|
|
577
|
-
if (!key) return true;
|
|
578
|
-
const client = await this.getReadyClient();
|
|
579
|
-
const response = await client.send(
|
|
580
|
-
new ListObjectsV2Command({
|
|
581
|
-
Bucket: this.bucket,
|
|
582
|
-
Prefix: key.replace(/\/$/, "") + "/",
|
|
583
|
-
MaxKeys: 1
|
|
584
|
-
})
|
|
585
|
-
);
|
|
586
|
-
return (response.Contents?.length ?? 0) > 0;
|
|
587
|
-
}
|
|
588
|
-
// ---------------------------------------------------------------------------
|
|
589
|
-
// Lifecycle (overrides base class protected methods)
|
|
590
|
-
// ---------------------------------------------------------------------------
|
|
591
|
-
/**
|
|
592
|
-
* Initialize the S3 client.
|
|
593
|
-
* Status management is handled by the base class.
|
|
594
|
-
*/
|
|
595
|
-
async init() {
|
|
596
|
-
const client = this.getClient();
|
|
597
|
-
try {
|
|
598
|
-
await client.send(new HeadBucketCommand({ Bucket: this.bucket }));
|
|
599
|
-
} catch (error) {
|
|
600
|
-
const statusCode = error.$metadata?.httpStatusCode;
|
|
601
|
-
const createError = (message2) => {
|
|
602
|
-
const err = new Error(message2);
|
|
603
|
-
if (statusCode) err.status = statusCode;
|
|
604
|
-
return err;
|
|
605
|
-
};
|
|
606
|
-
if (isAccessDeniedError(error)) {
|
|
607
|
-
throw createError(`Access denied to bucket "${this.bucket}" - check credentials and permissions`);
|
|
608
|
-
}
|
|
609
|
-
if (isNotFoundError(error)) {
|
|
610
|
-
throw createError(`Bucket "${this.bucket}" not found`);
|
|
611
|
-
}
|
|
612
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
613
|
-
if (statusCode) {
|
|
614
|
-
throw createError(`Failed to access bucket "${this.bucket}" (HTTP ${statusCode}): ${message}`);
|
|
615
|
-
}
|
|
616
|
-
throw error;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Clean up the S3 client.
|
|
621
|
-
* Status management is handled by the base class.
|
|
622
|
-
*/
|
|
623
|
-
async destroy() {
|
|
624
|
-
this._client = null;
|
|
625
|
-
}
|
|
114
|
+
id;
|
|
115
|
+
name = "S3Filesystem";
|
|
116
|
+
provider = "s3";
|
|
117
|
+
readOnly;
|
|
118
|
+
status = "pending";
|
|
119
|
+
displayName;
|
|
120
|
+
icon = "s3";
|
|
121
|
+
description;
|
|
122
|
+
bucket;
|
|
123
|
+
region;
|
|
124
|
+
credentials;
|
|
125
|
+
accessKeyId;
|
|
126
|
+
secretAccessKey;
|
|
127
|
+
sessionToken;
|
|
128
|
+
endpoint;
|
|
129
|
+
forcePathStyle;
|
|
130
|
+
prefix;
|
|
131
|
+
_client = null;
|
|
132
|
+
constructor(options) {
|
|
133
|
+
super({
|
|
134
|
+
...options,
|
|
135
|
+
name: "S3Filesystem"
|
|
136
|
+
});
|
|
137
|
+
this.id = options.id ?? `s3-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
138
|
+
this.bucket = options.bucket;
|
|
139
|
+
this.region = options.region;
|
|
140
|
+
this.credentials = options.credentials;
|
|
141
|
+
this.accessKeyId = options.accessKeyId;
|
|
142
|
+
this.secretAccessKey = options.secretAccessKey;
|
|
143
|
+
this.sessionToken = options.sessionToken;
|
|
144
|
+
this.endpoint = options.endpoint;
|
|
145
|
+
this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
|
|
146
|
+
const trimmedPrefix = options.prefix ? trimSlashes$1(options.prefix) : "";
|
|
147
|
+
this.prefix = trimmedPrefix ? trimmedPrefix + "/" : "";
|
|
148
|
+
this.icon = options.icon ?? this.detectIconFromEndpoint(options.endpoint);
|
|
149
|
+
this.displayName = options.displayName ?? this.getDefaultDisplayName(this.icon);
|
|
150
|
+
this.description = options.description;
|
|
151
|
+
this.readOnly = options.readOnly;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get the underlying S3Client instance for direct access to AWS S3 APIs.
|
|
155
|
+
*
|
|
156
|
+
* Use this when you need to access S3 features not exposed through the
|
|
157
|
+
* WorkspaceFilesystem interface (e.g., presigned URLs, multipart uploads,
|
|
158
|
+
* custom S3 operations, etc.).
|
|
159
|
+
*
|
|
160
|
+
* @example Generate a presigned URL
|
|
161
|
+
* ```typescript
|
|
162
|
+
* import { GetObjectCommand } from '@aws-sdk/client-s3';
|
|
163
|
+
* import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
164
|
+
*
|
|
165
|
+
* const s3Client = fs.client;
|
|
166
|
+
* const url = await getSignedUrl(s3Client, new GetObjectCommand({
|
|
167
|
+
* Bucket: 'my-bucket',
|
|
168
|
+
* Key: 'my-file.txt',
|
|
169
|
+
* }));
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
get client() {
|
|
173
|
+
return this.getClient();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get mount configuration for E2B sandbox.
|
|
177
|
+
* Returns S3-compatible config that works with s3fs-fuse.
|
|
178
|
+
*
|
|
179
|
+
* Only static `accessKeyId`/`secretAccessKey`/`sessionToken` are included in the
|
|
180
|
+
* returned config. If credentials are provided only via the `credentials` option
|
|
181
|
+
* (provider function), the returned config will have no credentials because FUSE
|
|
182
|
+
* mounts cannot call a provider function. Use static credentials for sandbox
|
|
183
|
+
* mount compatibility.
|
|
184
|
+
*/
|
|
185
|
+
getMountConfig() {
|
|
186
|
+
const config = {
|
|
187
|
+
type: "s3",
|
|
188
|
+
bucket: this.bucket,
|
|
189
|
+
region: this.region,
|
|
190
|
+
endpoint: this.endpoint
|
|
191
|
+
};
|
|
192
|
+
if (this.accessKeyId && this.secretAccessKey) {
|
|
193
|
+
config.accessKeyId = this.accessKeyId;
|
|
194
|
+
config.secretAccessKey = this.secretAccessKey;
|
|
195
|
+
if (this.sessionToken) config.sessionToken = this.sessionToken;
|
|
196
|
+
}
|
|
197
|
+
if (this.prefix) config.prefix = this.prefix;
|
|
198
|
+
if (this.readOnly) config.readOnly = true;
|
|
199
|
+
return config;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get filesystem info for status reporting.
|
|
203
|
+
*/
|
|
204
|
+
getInfo() {
|
|
205
|
+
return {
|
|
206
|
+
id: this.id,
|
|
207
|
+
name: this.name,
|
|
208
|
+
provider: this.provider,
|
|
209
|
+
status: this.status,
|
|
210
|
+
error: this.error,
|
|
211
|
+
readOnly: this.readOnly,
|
|
212
|
+
icon: this.icon,
|
|
213
|
+
metadata: {
|
|
214
|
+
bucket: this.bucket,
|
|
215
|
+
region: this.region,
|
|
216
|
+
...this.endpoint && { endpoint: this.endpoint },
|
|
217
|
+
...this.prefix && { prefix: this.prefix }
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Handle an error, checking for access denied and updating status accordingly.
|
|
223
|
+
* Returns the error for re-throwing.
|
|
224
|
+
*/
|
|
225
|
+
handleError(error) {
|
|
226
|
+
if (isAccessDeniedError(error)) {
|
|
227
|
+
this.status = "error";
|
|
228
|
+
this.error = "Access denied - check credentials and bucket permissions";
|
|
229
|
+
}
|
|
230
|
+
return error;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get instructions describing this S3 filesystem.
|
|
234
|
+
* Used by agents to understand storage semantics.
|
|
235
|
+
*/
|
|
236
|
+
getInstructions() {
|
|
237
|
+
const providerName = this.displayName || "S3";
|
|
238
|
+
const access = this.readOnly ? "Read-only" : "Persistent";
|
|
239
|
+
return `${providerName} storage in bucket "${this.bucket}". ${access} storage - files are retained across sessions.`;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Detect the appropriate icon based on the S3 endpoint.
|
|
243
|
+
*/
|
|
244
|
+
detectIconFromEndpoint(endpoint) {
|
|
245
|
+
if (!endpoint) return "aws-s3";
|
|
246
|
+
let hostname;
|
|
247
|
+
try {
|
|
248
|
+
hostname = new URL(endpoint).hostname.toLowerCase();
|
|
249
|
+
} catch {
|
|
250
|
+
hostname = endpoint.toLowerCase();
|
|
251
|
+
}
|
|
252
|
+
if (hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com") || hostname.endsWith(".cloudflare.com")) return "r2";
|
|
253
|
+
if (hostname === "storage.googleapis.com" || hostname.endsWith(".storage.googleapis.com") || hostname.endsWith(".googleapis.com")) return "gcs";
|
|
254
|
+
if (hostname === "blob.core.windows.net" || hostname.endsWith(".blob.core.windows.net") || hostname.endsWith(".azure.com")) return "azure";
|
|
255
|
+
if (hostname.includes("minio")) return "minio";
|
|
256
|
+
return "s3";
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get a user-friendly display name based on the icon/provider.
|
|
260
|
+
*/
|
|
261
|
+
getDefaultDisplayName(icon) {
|
|
262
|
+
switch (icon) {
|
|
263
|
+
case "aws-s3": return "AWS S3";
|
|
264
|
+
case "r2":
|
|
265
|
+
case "cloudflare":
|
|
266
|
+
case "cloudflare-r2": return "Cloudflare R2";
|
|
267
|
+
case "gcs":
|
|
268
|
+
case "google-cloud":
|
|
269
|
+
case "google-cloud-storage": return "Google Cloud Storage";
|
|
270
|
+
case "azure":
|
|
271
|
+
case "azure-blob": return "Azure Blob";
|
|
272
|
+
case "minio": return "MinIO";
|
|
273
|
+
case "s3": return "S3";
|
|
274
|
+
default: return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
getClient() {
|
|
278
|
+
if (this._client) return this._client;
|
|
279
|
+
const hasStaticCredentials = this.accessKeyId && this.secretAccessKey;
|
|
280
|
+
let credentials;
|
|
281
|
+
if (this.credentials) credentials = this.credentials;
|
|
282
|
+
else if (hasStaticCredentials) credentials = {
|
|
283
|
+
accessKeyId: this.accessKeyId,
|
|
284
|
+
secretAccessKey: this.secretAccessKey,
|
|
285
|
+
...this.sessionToken && { sessionToken: this.sessionToken }
|
|
286
|
+
};
|
|
287
|
+
this._client = new S3Client({
|
|
288
|
+
region: this.region,
|
|
289
|
+
...credentials !== void 0 && { credentials },
|
|
290
|
+
endpoint: this.endpoint,
|
|
291
|
+
forcePathStyle: this.forcePathStyle
|
|
292
|
+
});
|
|
293
|
+
return this._client;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Ensure the filesystem is initialized and return the S3 client.
|
|
297
|
+
* Uses base class ensureReady() for status management, then returns client.
|
|
298
|
+
*/
|
|
299
|
+
async getReadyClient() {
|
|
300
|
+
await this.ensureReady();
|
|
301
|
+
return this.getClient();
|
|
302
|
+
}
|
|
303
|
+
toKey(path) {
|
|
304
|
+
const cleanPath = path.replace(/^\/+/, "").replace(/^\.(?:\/|$)/, "");
|
|
305
|
+
return this.prefix + cleanPath;
|
|
306
|
+
}
|
|
307
|
+
async readFile(path, options) {
|
|
308
|
+
const client = await this.getReadyClient();
|
|
309
|
+
try {
|
|
310
|
+
const body = await (await client.send(new GetObjectCommand({
|
|
311
|
+
Bucket: this.bucket,
|
|
312
|
+
Key: this.toKey(path)
|
|
313
|
+
}))).Body?.transformToByteArray();
|
|
314
|
+
if (!body) throw new FileNotFoundError(path);
|
|
315
|
+
const buffer = Buffer.from(body);
|
|
316
|
+
if (options?.encoding) return buffer.toString(options.encoding);
|
|
317
|
+
return buffer;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (isNotFoundError$1(error)) throw new FileNotFoundError(path);
|
|
320
|
+
throw this.handleError(error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async writeFile(path, content, options) {
|
|
324
|
+
const client = await this.getReadyClient();
|
|
325
|
+
if (options?.overwrite === false && await this.exists(path)) throw new FileExistsError(path);
|
|
326
|
+
const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
|
|
327
|
+
const contentType = getMimeType(path);
|
|
328
|
+
await client.send(new PutObjectCommand({
|
|
329
|
+
Bucket: this.bucket,
|
|
330
|
+
Key: this.toKey(path),
|
|
331
|
+
Body: body,
|
|
332
|
+
ContentType: contentType
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
async appendFile(path, content) {
|
|
336
|
+
let existing = "";
|
|
337
|
+
try {
|
|
338
|
+
existing = await this.readFile(path, { encoding: "utf-8" });
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (error instanceof FileNotFoundError) {} else throw error;
|
|
341
|
+
}
|
|
342
|
+
const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
|
|
343
|
+
await this.writeFile(path, existing + appendContent);
|
|
344
|
+
}
|
|
345
|
+
async deleteFile(path, options) {
|
|
346
|
+
if (await this.isDirectory(path)) {
|
|
347
|
+
await this.rmdir(path, {
|
|
348
|
+
recursive: true,
|
|
349
|
+
force: options?.force
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const client = await this.getReadyClient();
|
|
354
|
+
try {
|
|
355
|
+
await client.send(new DeleteObjectCommand({
|
|
356
|
+
Bucket: this.bucket,
|
|
357
|
+
Key: this.toKey(path)
|
|
358
|
+
}));
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (options?.force) return;
|
|
361
|
+
if (isNotFoundError$1(error)) throw new FileNotFoundError(path);
|
|
362
|
+
throw this.handleError(error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async copyFile(src, dest, options) {
|
|
366
|
+
const client = await this.getReadyClient();
|
|
367
|
+
if (options?.overwrite === false && await this.exists(dest)) throw new FileExistsError(dest);
|
|
368
|
+
try {
|
|
369
|
+
await client.send(new CopyObjectCommand({
|
|
370
|
+
Bucket: this.bucket,
|
|
371
|
+
CopySource: `${this.bucket}/${encodeURIComponent(this.toKey(src)).replace(/%2F/g, "/")}`,
|
|
372
|
+
Key: this.toKey(dest)
|
|
373
|
+
}));
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (isNotFoundError$1(error)) throw new FileNotFoundError(src);
|
|
376
|
+
throw this.handleError(error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async moveFile(src, dest, options) {
|
|
380
|
+
await this.copyFile(src, dest, options);
|
|
381
|
+
await this.deleteFile(src, { force: true });
|
|
382
|
+
}
|
|
383
|
+
async mkdir(_path, _options) {}
|
|
384
|
+
async rmdir(path, options) {
|
|
385
|
+
if (!options?.recursive) {
|
|
386
|
+
if ((await this.readdir(path)).length > 0) throw new Error(`Directory not empty: ${path}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const client = await this.getReadyClient();
|
|
390
|
+
const prefix = this.toKey(path).replace(/\/$/, "") + "/";
|
|
391
|
+
let continuationToken;
|
|
392
|
+
do {
|
|
393
|
+
const listResponse = await client.send(new ListObjectsV2Command({
|
|
394
|
+
Bucket: this.bucket,
|
|
395
|
+
Prefix: prefix,
|
|
396
|
+
ContinuationToken: continuationToken
|
|
397
|
+
}));
|
|
398
|
+
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
|
399
|
+
const deleteResponse = await client.send(new DeleteObjectsCommand({
|
|
400
|
+
Bucket: this.bucket,
|
|
401
|
+
Delete: { Objects: listResponse.Contents.filter((obj) => !!obj.Key).map((obj) => ({ Key: obj.Key })) }
|
|
402
|
+
}));
|
|
403
|
+
if (deleteResponse.Errors && deleteResponse.Errors.length > 0) throw new Error(`Failed to delete ${deleteResponse.Errors.length} object(s) in ${path}`);
|
|
404
|
+
}
|
|
405
|
+
continuationToken = listResponse.NextContinuationToken;
|
|
406
|
+
} while (continuationToken);
|
|
407
|
+
}
|
|
408
|
+
async readdir(path, options) {
|
|
409
|
+
const client = await this.getReadyClient();
|
|
410
|
+
const prefix = this.toKey(path).replace(/\/$/, "");
|
|
411
|
+
const searchPrefix = prefix ? prefix + "/" : "";
|
|
412
|
+
const entries = [];
|
|
413
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
414
|
+
let continuationToken;
|
|
415
|
+
do {
|
|
416
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
417
|
+
Bucket: this.bucket,
|
|
418
|
+
Prefix: searchPrefix,
|
|
419
|
+
Delimiter: options?.recursive ? void 0 : "/",
|
|
420
|
+
ContinuationToken: continuationToken
|
|
421
|
+
}));
|
|
422
|
+
if (response.Contents) for (const obj of response.Contents) {
|
|
423
|
+
const key = obj.Key;
|
|
424
|
+
if (!key || key === searchPrefix) continue;
|
|
425
|
+
const relativePath = key.slice(searchPrefix.length);
|
|
426
|
+
if (!relativePath) continue;
|
|
427
|
+
if (relativePath.endsWith("/")) {
|
|
428
|
+
const dirName = relativePath.slice(0, -1);
|
|
429
|
+
if (!seenDirs.has(dirName)) {
|
|
430
|
+
seenDirs.add(dirName);
|
|
431
|
+
entries.push({
|
|
432
|
+
name: dirName,
|
|
433
|
+
type: "directory"
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const name = options?.recursive ? relativePath : relativePath.split("/")[0];
|
|
439
|
+
if (!name) continue;
|
|
440
|
+
if (options?.extension) {
|
|
441
|
+
if (!(Array.isArray(options.extension) ? options.extension : [options.extension]).some((ext) => name.endsWith(ext))) continue;
|
|
442
|
+
}
|
|
443
|
+
entries.push({
|
|
444
|
+
name,
|
|
445
|
+
type: "file",
|
|
446
|
+
size: obj.Size
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (response.CommonPrefixes) for (const prefixObj of response.CommonPrefixes) {
|
|
450
|
+
if (!prefixObj.Prefix) continue;
|
|
451
|
+
const dirName = prefixObj.Prefix.slice(searchPrefix.length).replace(/\/$/, "");
|
|
452
|
+
if (dirName && !seenDirs.has(dirName)) {
|
|
453
|
+
seenDirs.add(dirName);
|
|
454
|
+
entries.push({
|
|
455
|
+
name: dirName,
|
|
456
|
+
type: "directory"
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
continuationToken = response.NextContinuationToken;
|
|
461
|
+
} while (continuationToken);
|
|
462
|
+
return entries;
|
|
463
|
+
}
|
|
464
|
+
async exists(path) {
|
|
465
|
+
const key = this.toKey(path);
|
|
466
|
+
if (!key) return true;
|
|
467
|
+
const client = await this.getReadyClient();
|
|
468
|
+
try {
|
|
469
|
+
await client.send(new HeadObjectCommand({
|
|
470
|
+
Bucket: this.bucket,
|
|
471
|
+
Key: key
|
|
472
|
+
}));
|
|
473
|
+
return true;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (!isNotFoundError$1(error)) throw this.handleError(error);
|
|
476
|
+
}
|
|
477
|
+
return ((await client.send(new ListObjectsV2Command({
|
|
478
|
+
Bucket: this.bucket,
|
|
479
|
+
Prefix: key.replace(/\/$/, "") + "/",
|
|
480
|
+
MaxKeys: 1
|
|
481
|
+
}))).Contents?.length ?? 0) > 0;
|
|
482
|
+
}
|
|
483
|
+
async stat(path) {
|
|
484
|
+
const key = this.toKey(path);
|
|
485
|
+
if (!key) return {
|
|
486
|
+
name: "",
|
|
487
|
+
path,
|
|
488
|
+
type: "directory",
|
|
489
|
+
size: 0,
|
|
490
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
491
|
+
modifiedAt: /* @__PURE__ */ new Date()
|
|
492
|
+
};
|
|
493
|
+
const client = await this.getReadyClient();
|
|
494
|
+
try {
|
|
495
|
+
const response = await client.send(new HeadObjectCommand({
|
|
496
|
+
Bucket: this.bucket,
|
|
497
|
+
Key: key
|
|
498
|
+
}));
|
|
499
|
+
return {
|
|
500
|
+
name: path.split("/").pop() ?? "",
|
|
501
|
+
path,
|
|
502
|
+
type: "file",
|
|
503
|
+
size: response.ContentLength ?? 0,
|
|
504
|
+
createdAt: response.LastModified ?? /* @__PURE__ */ new Date(),
|
|
505
|
+
modifiedAt: response.LastModified ?? /* @__PURE__ */ new Date()
|
|
506
|
+
};
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (!isNotFoundError$1(error)) throw this.handleError(error);
|
|
509
|
+
if (await this.isDirectory(path)) return {
|
|
510
|
+
name: path.split("/").filter(Boolean).pop() ?? "",
|
|
511
|
+
path,
|
|
512
|
+
type: "directory",
|
|
513
|
+
size: 0,
|
|
514
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
515
|
+
modifiedAt: /* @__PURE__ */ new Date()
|
|
516
|
+
};
|
|
517
|
+
throw new FileNotFoundError(path);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async isFile(path) {
|
|
521
|
+
const key = this.toKey(path);
|
|
522
|
+
if (!key) return false;
|
|
523
|
+
const client = await this.getReadyClient();
|
|
524
|
+
try {
|
|
525
|
+
await client.send(new HeadObjectCommand({
|
|
526
|
+
Bucket: this.bucket,
|
|
527
|
+
Key: key
|
|
528
|
+
}));
|
|
529
|
+
return true;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (!isNotFoundError$1(error)) throw this.handleError(error);
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async isDirectory(path) {
|
|
536
|
+
const key = this.toKey(path);
|
|
537
|
+
if (!key) return true;
|
|
538
|
+
return ((await (await this.getReadyClient()).send(new ListObjectsV2Command({
|
|
539
|
+
Bucket: this.bucket,
|
|
540
|
+
Prefix: key.replace(/\/$/, "") + "/",
|
|
541
|
+
MaxKeys: 1
|
|
542
|
+
}))).Contents?.length ?? 0) > 0;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Initialize the S3 client.
|
|
546
|
+
* Status management is handled by the base class.
|
|
547
|
+
*/
|
|
548
|
+
async init() {
|
|
549
|
+
const client = this.getClient();
|
|
550
|
+
try {
|
|
551
|
+
await client.send(new HeadBucketCommand({ Bucket: this.bucket }));
|
|
552
|
+
} catch (error) {
|
|
553
|
+
const statusCode = error.$metadata?.httpStatusCode;
|
|
554
|
+
const createError = (message) => {
|
|
555
|
+
const err = new Error(message);
|
|
556
|
+
if (statusCode) err.status = statusCode;
|
|
557
|
+
return err;
|
|
558
|
+
};
|
|
559
|
+
if (isAccessDeniedError(error)) throw createError(`Access denied to bucket "${this.bucket}" - check credentials and permissions`);
|
|
560
|
+
if (isNotFoundError$1(error)) throw createError(`Bucket "${this.bucket}" not found`);
|
|
561
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
562
|
+
if (statusCode) throw createError(`Failed to access bucket "${this.bucket}" (HTTP ${statusCode}): ${message}`);
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Clean up the S3 client.
|
|
568
|
+
* Status management is handled by the base class.
|
|
569
|
+
*/
|
|
570
|
+
async destroy() {
|
|
571
|
+
this._client = null;
|
|
572
|
+
}
|
|
626
573
|
};
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/blob-store/index.ts
|
|
576
|
+
/** Trim leading and trailing slashes. */
|
|
577
|
+
function trimSlashes(s) {
|
|
578
|
+
let start = 0;
|
|
579
|
+
let end = s.length;
|
|
580
|
+
while (start < end && s[start] === "/") start++;
|
|
581
|
+
while (end > start && s[end - 1] === "/") end--;
|
|
582
|
+
return s.slice(start, end);
|
|
633
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* S3-backed content-addressable blob store for skill versioning.
|
|
586
|
+
*
|
|
587
|
+
* Each blob is stored as an S3 object keyed by its SHA-256 hash.
|
|
588
|
+
* Metadata (size, mimeType, createdAt) is stored in S3 object user metadata.
|
|
589
|
+
*
|
|
590
|
+
* Since blobs are content-addressable, writes are idempotent — the same hash
|
|
591
|
+
* always maps to the same content, so overwrites are safe and equivalent to
|
|
592
|
+
* a no-op.
|
|
593
|
+
*
|
|
594
|
+
* @example AWS S3
|
|
595
|
+
* ```typescript
|
|
596
|
+
* import { S3BlobStore } from '@mastra/s3';
|
|
597
|
+
*
|
|
598
|
+
* const blobs = new S3BlobStore({
|
|
599
|
+
* bucket: 'my-skill-blobs',
|
|
600
|
+
* region: 'us-east-1',
|
|
601
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
602
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
603
|
+
* });
|
|
604
|
+
* ```
|
|
605
|
+
*
|
|
606
|
+
* @example MinIO (local)
|
|
607
|
+
* ```typescript
|
|
608
|
+
* import { S3BlobStore } from '@mastra/s3';
|
|
609
|
+
*
|
|
610
|
+
* const blobs = new S3BlobStore({
|
|
611
|
+
* bucket: 'skill-blobs',
|
|
612
|
+
* region: 'us-east-1',
|
|
613
|
+
* accessKeyId: 'minioadmin',
|
|
614
|
+
* secretAccessKey: 'minioadmin',
|
|
615
|
+
* endpoint: 'http://localhost:9000',
|
|
616
|
+
* forcePathStyle: true,
|
|
617
|
+
* });
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
634
620
|
var S3BlobStore = class extends BlobStore {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
let continuationToken;
|
|
769
|
-
do {
|
|
770
|
-
const listResponse = await client.send(
|
|
771
|
-
new ListObjectsV2Command({
|
|
772
|
-
Bucket: this.bucket,
|
|
773
|
-
Prefix: this.prefix,
|
|
774
|
-
ContinuationToken: continuationToken
|
|
775
|
-
})
|
|
776
|
-
);
|
|
777
|
-
const objects = listResponse.Contents;
|
|
778
|
-
if (objects && objects.length > 0) {
|
|
779
|
-
await client.send(
|
|
780
|
-
new DeleteObjectsCommand({
|
|
781
|
-
Bucket: this.bucket,
|
|
782
|
-
Delete: {
|
|
783
|
-
Objects: objects.filter((obj) => obj.Key != null).map((obj) => ({ Key: obj.Key })),
|
|
784
|
-
Quiet: true
|
|
785
|
-
}
|
|
786
|
-
})
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : void 0;
|
|
790
|
-
} while (continuationToken);
|
|
791
|
-
}
|
|
621
|
+
bucket;
|
|
622
|
+
prefix;
|
|
623
|
+
_client = null;
|
|
624
|
+
region;
|
|
625
|
+
credentials;
|
|
626
|
+
accessKeyId;
|
|
627
|
+
secretAccessKey;
|
|
628
|
+
sessionToken;
|
|
629
|
+
endpoint;
|
|
630
|
+
forcePathStyle;
|
|
631
|
+
constructor(options) {
|
|
632
|
+
super();
|
|
633
|
+
this.bucket = options.bucket;
|
|
634
|
+
this.region = options.region;
|
|
635
|
+
this.credentials = options.credentials;
|
|
636
|
+
this.accessKeyId = options.accessKeyId;
|
|
637
|
+
this.secretAccessKey = options.secretAccessKey;
|
|
638
|
+
this.sessionToken = options.sessionToken;
|
|
639
|
+
this.endpoint = options.endpoint;
|
|
640
|
+
this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
|
|
641
|
+
this.prefix = options.prefix ? trimSlashes(options.prefix) + "/" : "mastra_skill_blobs/";
|
|
642
|
+
}
|
|
643
|
+
getClient() {
|
|
644
|
+
if (this._client) return this._client;
|
|
645
|
+
const hasStaticCredentials = this.accessKeyId && this.secretAccessKey;
|
|
646
|
+
let credentials;
|
|
647
|
+
if (this.credentials) credentials = this.credentials;
|
|
648
|
+
else if (hasStaticCredentials) credentials = {
|
|
649
|
+
accessKeyId: this.accessKeyId,
|
|
650
|
+
secretAccessKey: this.secretAccessKey,
|
|
651
|
+
...this.sessionToken && { sessionToken: this.sessionToken }
|
|
652
|
+
};
|
|
653
|
+
this._client = new S3Client({
|
|
654
|
+
region: this.region,
|
|
655
|
+
...credentials !== void 0 && { credentials },
|
|
656
|
+
endpoint: this.endpoint,
|
|
657
|
+
forcePathStyle: this.forcePathStyle
|
|
658
|
+
});
|
|
659
|
+
return this._client;
|
|
660
|
+
}
|
|
661
|
+
toKey(hash) {
|
|
662
|
+
return this.prefix + hash;
|
|
663
|
+
}
|
|
664
|
+
async init() {}
|
|
665
|
+
async put(entry) {
|
|
666
|
+
const client = this.getClient();
|
|
667
|
+
const now = entry.createdAt ?? /* @__PURE__ */ new Date();
|
|
668
|
+
await client.send(new PutObjectCommand({
|
|
669
|
+
Bucket: this.bucket,
|
|
670
|
+
Key: this.toKey(entry.hash),
|
|
671
|
+
Body: entry.content,
|
|
672
|
+
ContentType: entry.mimeType ?? "application/octet-stream",
|
|
673
|
+
Metadata: {
|
|
674
|
+
size: String(entry.size),
|
|
675
|
+
createdat: now.toISOString(),
|
|
676
|
+
...entry.mimeType ? { mimetype: entry.mimeType } : {}
|
|
677
|
+
}
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
async get(hash) {
|
|
681
|
+
const client = this.getClient();
|
|
682
|
+
try {
|
|
683
|
+
const response = await client.send(new GetObjectCommand({
|
|
684
|
+
Bucket: this.bucket,
|
|
685
|
+
Key: this.toKey(hash)
|
|
686
|
+
}));
|
|
687
|
+
const body = await response.Body?.transformToString("utf-8");
|
|
688
|
+
if (body === void 0 || body === null) return null;
|
|
689
|
+
const metadata = response.Metadata ?? {};
|
|
690
|
+
return {
|
|
691
|
+
hash,
|
|
692
|
+
content: body,
|
|
693
|
+
size: metadata.size != null ? Number(metadata.size) : Buffer.byteLength(body, "utf-8"),
|
|
694
|
+
mimeType: metadata.mimetype || response.ContentType || void 0,
|
|
695
|
+
createdAt: metadata.createdat ? new Date(metadata.createdat) : /* @__PURE__ */ new Date()
|
|
696
|
+
};
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (isNotFoundError(error)) return null;
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async has(hash) {
|
|
703
|
+
const client = this.getClient();
|
|
704
|
+
try {
|
|
705
|
+
await client.send(new HeadObjectCommand({
|
|
706
|
+
Bucket: this.bucket,
|
|
707
|
+
Key: this.toKey(hash)
|
|
708
|
+
}));
|
|
709
|
+
return true;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (isNotFoundError(error)) return false;
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async delete(hash) {
|
|
716
|
+
if (!await this.has(hash)) return false;
|
|
717
|
+
await this.getClient().send(new DeleteObjectCommand({
|
|
718
|
+
Bucket: this.bucket,
|
|
719
|
+
Key: this.toKey(hash)
|
|
720
|
+
}));
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
async putMany(entries) {
|
|
724
|
+
if (entries.length === 0) return;
|
|
725
|
+
await Promise.all(entries.map((entry) => this.put(entry)));
|
|
726
|
+
}
|
|
727
|
+
async getMany(hashes) {
|
|
728
|
+
const result = /* @__PURE__ */ new Map();
|
|
729
|
+
if (hashes.length === 0) return result;
|
|
730
|
+
const entries = await Promise.all(hashes.map((hash) => this.get(hash)));
|
|
731
|
+
for (const entry of entries) if (entry) result.set(entry.hash, entry);
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
async dangerouslyClearAll() {
|
|
735
|
+
const client = this.getClient();
|
|
736
|
+
let continuationToken;
|
|
737
|
+
do {
|
|
738
|
+
const listResponse = await client.send(new ListObjectsV2Command({
|
|
739
|
+
Bucket: this.bucket,
|
|
740
|
+
Prefix: this.prefix,
|
|
741
|
+
ContinuationToken: continuationToken
|
|
742
|
+
}));
|
|
743
|
+
const objects = listResponse.Contents;
|
|
744
|
+
if (objects && objects.length > 0) await client.send(new DeleteObjectsCommand({
|
|
745
|
+
Bucket: this.bucket,
|
|
746
|
+
Delete: {
|
|
747
|
+
Objects: objects.filter((obj) => obj.Key != null).map((obj) => ({ Key: obj.Key })),
|
|
748
|
+
Quiet: true
|
|
749
|
+
}
|
|
750
|
+
}));
|
|
751
|
+
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : void 0;
|
|
752
|
+
} while (continuationToken);
|
|
753
|
+
}
|
|
792
754
|
};
|
|
793
|
-
function
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
755
|
+
function isNotFoundError(error) {
|
|
756
|
+
if (!error || typeof error !== "object" || !("name" in error)) return false;
|
|
757
|
+
const name = error.name;
|
|
758
|
+
return name === "NotFound" || name === "NoSuchKey" || name === "404";
|
|
797
759
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
760
|
+
//#endregion
|
|
761
|
+
//#region src/provider.ts
|
|
762
|
+
const s3FilesystemProvider = {
|
|
763
|
+
id: "s3",
|
|
764
|
+
name: "Amazon S3",
|
|
765
|
+
description: "S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)",
|
|
766
|
+
configSchema: {
|
|
767
|
+
type: "object",
|
|
768
|
+
required: ["bucket", "region"],
|
|
769
|
+
properties: {
|
|
770
|
+
bucket: {
|
|
771
|
+
type: "string",
|
|
772
|
+
description: "S3 bucket name"
|
|
773
|
+
},
|
|
774
|
+
region: {
|
|
775
|
+
type: "string",
|
|
776
|
+
description: "AWS region (use \"auto\" for R2)"
|
|
777
|
+
},
|
|
778
|
+
accessKeyId: {
|
|
779
|
+
type: "string",
|
|
780
|
+
description: "AWS access key ID"
|
|
781
|
+
},
|
|
782
|
+
secretAccessKey: {
|
|
783
|
+
type: "string",
|
|
784
|
+
description: "AWS secret access key"
|
|
785
|
+
},
|
|
786
|
+
sessionToken: {
|
|
787
|
+
type: "string",
|
|
788
|
+
description: "AWS session token for temporary credentials"
|
|
789
|
+
},
|
|
790
|
+
endpoint: {
|
|
791
|
+
type: "string",
|
|
792
|
+
description: "Custom endpoint URL for S3-compatible storage"
|
|
793
|
+
},
|
|
794
|
+
forcePathStyle: {
|
|
795
|
+
type: "boolean",
|
|
796
|
+
description: "Force path-style URLs",
|
|
797
|
+
default: false
|
|
798
|
+
},
|
|
799
|
+
prefix: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: "Key prefix (acts like a subdirectory)"
|
|
802
|
+
},
|
|
803
|
+
readOnly: {
|
|
804
|
+
type: "boolean",
|
|
805
|
+
description: "Mount as read-only",
|
|
806
|
+
default: false
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
createFilesystem: (config) => new S3Filesystem(config)
|
|
820
811
|
};
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
812
|
+
/**
|
|
813
|
+
* S3 blob store provider descriptor for MastraEditor.
|
|
814
|
+
*
|
|
815
|
+
* @example
|
|
816
|
+
* ```typescript
|
|
817
|
+
* import { s3BlobStoreProvider } from '@mastra/s3';
|
|
818
|
+
*
|
|
819
|
+
* const editor = new MastraEditor({
|
|
820
|
+
* blobStores: { s3: s3BlobStoreProvider },
|
|
821
|
+
* });
|
|
822
|
+
* ```
|
|
823
|
+
*/
|
|
824
|
+
const s3BlobStoreProvider = {
|
|
825
|
+
id: "s3",
|
|
826
|
+
name: "Amazon S3 Blob Store",
|
|
827
|
+
description: "Content-addressable blob storage using S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)",
|
|
828
|
+
configSchema: {
|
|
829
|
+
type: "object",
|
|
830
|
+
required: [
|
|
831
|
+
"bucket",
|
|
832
|
+
"region",
|
|
833
|
+
"accessKeyId",
|
|
834
|
+
"secretAccessKey"
|
|
835
|
+
],
|
|
836
|
+
properties: {
|
|
837
|
+
bucket: {
|
|
838
|
+
type: "string",
|
|
839
|
+
description: "S3 bucket name"
|
|
840
|
+
},
|
|
841
|
+
region: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "AWS region (use \"auto\" for R2)"
|
|
844
|
+
},
|
|
845
|
+
accessKeyId: {
|
|
846
|
+
type: "string",
|
|
847
|
+
description: "AWS access key ID"
|
|
848
|
+
},
|
|
849
|
+
secretAccessKey: {
|
|
850
|
+
type: "string",
|
|
851
|
+
description: "AWS secret access key"
|
|
852
|
+
},
|
|
853
|
+
sessionToken: {
|
|
854
|
+
type: "string",
|
|
855
|
+
description: "AWS session token for temporary credentials"
|
|
856
|
+
},
|
|
857
|
+
endpoint: {
|
|
858
|
+
type: "string",
|
|
859
|
+
description: "Custom endpoint URL for S3-compatible storage"
|
|
860
|
+
},
|
|
861
|
+
forcePathStyle: {
|
|
862
|
+
type: "boolean",
|
|
863
|
+
description: "Force path-style URLs",
|
|
864
|
+
default: false
|
|
865
|
+
},
|
|
866
|
+
prefix: {
|
|
867
|
+
type: "string",
|
|
868
|
+
description: "Key prefix for blob objects (default: mastra_skill_blobs/)"
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
createBlobStore: (config) => new S3BlobStore(config)
|
|
840
873
|
};
|
|
841
|
-
|
|
874
|
+
//#endregion
|
|
842
875
|
export { S3BlobStore, S3Filesystem, s3BlobStoreProvider, s3FilesystemProvider };
|
|
843
|
-
|
|
876
|
+
|
|
844
877
|
//# sourceMappingURL=index.js.map
|