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