@mastra/s3 0.0.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/LICENSE.md +15 -0
- package/README.md +72 -0
- package/dist/filesystem/index.d.ts +141 -0
- package/dist/filesystem/index.d.ts.map +1 -0
- package/dist/index.cjs +586 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +584 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var clientS3 = require('@aws-sdk/client-s3');
|
|
4
|
+
var workspace = require('@mastra/core/workspace');
|
|
5
|
+
|
|
6
|
+
// src/filesystem/index.ts
|
|
7
|
+
var MIME_TYPES = {
|
|
8
|
+
// Text
|
|
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
|
+
// Code
|
|
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
|
+
// Images
|
|
31
|
+
".png": "image/png",
|
|
32
|
+
".jpg": "image/jpeg",
|
|
33
|
+
".jpeg": "image/jpeg",
|
|
34
|
+
".gif": "image/gif",
|
|
35
|
+
".svg": "image/svg+xml",
|
|
36
|
+
".webp": "image/webp",
|
|
37
|
+
".ico": "image/x-icon",
|
|
38
|
+
// Documents
|
|
39
|
+
".pdf": "application/pdf",
|
|
40
|
+
// Archives
|
|
41
|
+
".zip": "application/zip",
|
|
42
|
+
".gz": "application/gzip",
|
|
43
|
+
".tar": "application/x-tar"
|
|
44
|
+
};
|
|
45
|
+
function getMimeType(path) {
|
|
46
|
+
const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
|
|
47
|
+
return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
|
|
48
|
+
}
|
|
49
|
+
function isNotFoundError(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";
|
|
53
|
+
}
|
|
54
|
+
function isAccessDeniedError(error) {
|
|
55
|
+
if (!error || typeof error !== "object") return false;
|
|
56
|
+
const err = error;
|
|
57
|
+
return err.name === "AccessDenied" || err.$metadata?.httpStatusCode === 403;
|
|
58
|
+
}
|
|
59
|
+
function trimSlashes(s) {
|
|
60
|
+
let start = 0;
|
|
61
|
+
let end = s.length;
|
|
62
|
+
while (start < end && s[start] === "/") start++;
|
|
63
|
+
while (end > start && s[end - 1] === "/") end--;
|
|
64
|
+
return s.slice(start, end);
|
|
65
|
+
}
|
|
66
|
+
var S3Filesystem = class extends workspace.MastraFilesystem {
|
|
67
|
+
id;
|
|
68
|
+
name = "S3Filesystem";
|
|
69
|
+
provider = "s3";
|
|
70
|
+
readOnly;
|
|
71
|
+
status = "pending";
|
|
72
|
+
// Display metadata for UI
|
|
73
|
+
displayName;
|
|
74
|
+
icon = "s3";
|
|
75
|
+
description;
|
|
76
|
+
bucket;
|
|
77
|
+
region;
|
|
78
|
+
accessKeyId;
|
|
79
|
+
secretAccessKey;
|
|
80
|
+
endpoint;
|
|
81
|
+
forcePathStyle;
|
|
82
|
+
prefix;
|
|
83
|
+
_client = null;
|
|
84
|
+
constructor(options) {
|
|
85
|
+
super({ name: "S3Filesystem" });
|
|
86
|
+
this.id = options.id ?? `s3-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
87
|
+
this.bucket = options.bucket;
|
|
88
|
+
this.region = options.region;
|
|
89
|
+
this.accessKeyId = options.accessKeyId;
|
|
90
|
+
this.secretAccessKey = options.secretAccessKey;
|
|
91
|
+
this.endpoint = options.endpoint;
|
|
92
|
+
this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
|
|
93
|
+
this.prefix = options.prefix ? trimSlashes(options.prefix) + "/" : "";
|
|
94
|
+
this.icon = options.icon ?? this.detectIconFromEndpoint(options.endpoint);
|
|
95
|
+
this.displayName = options.displayName ?? this.getDefaultDisplayName(this.icon);
|
|
96
|
+
this.description = options.description;
|
|
97
|
+
this.readOnly = options.readOnly;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get mount configuration for E2B sandbox.
|
|
101
|
+
* Returns S3-compatible config that works with s3fs-fuse.
|
|
102
|
+
*/
|
|
103
|
+
getMountConfig() {
|
|
104
|
+
const config = {
|
|
105
|
+
type: "s3",
|
|
106
|
+
bucket: this.bucket,
|
|
107
|
+
region: this.region,
|
|
108
|
+
endpoint: this.endpoint
|
|
109
|
+
};
|
|
110
|
+
if (this.accessKeyId && this.secretAccessKey) {
|
|
111
|
+
config.accessKeyId = this.accessKeyId;
|
|
112
|
+
config.secretAccessKey = this.secretAccessKey;
|
|
113
|
+
}
|
|
114
|
+
if (this.readOnly) {
|
|
115
|
+
config.readOnly = true;
|
|
116
|
+
}
|
|
117
|
+
return config;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get filesystem info for status reporting.
|
|
121
|
+
*/
|
|
122
|
+
getInfo() {
|
|
123
|
+
return {
|
|
124
|
+
id: this.id,
|
|
125
|
+
name: this.name,
|
|
126
|
+
provider: this.provider,
|
|
127
|
+
status: this.status,
|
|
128
|
+
error: this.error,
|
|
129
|
+
icon: this.icon,
|
|
130
|
+
metadata: {
|
|
131
|
+
bucket: this.bucket,
|
|
132
|
+
region: this.region,
|
|
133
|
+
...this.endpoint && { endpoint: this.endpoint },
|
|
134
|
+
...this.prefix && { prefix: this.prefix }
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Handle an error, checking for access denied and updating status accordingly.
|
|
140
|
+
* Returns the error for re-throwing.
|
|
141
|
+
*/
|
|
142
|
+
handleError(error) {
|
|
143
|
+
if (isAccessDeniedError(error)) {
|
|
144
|
+
this.status = "error";
|
|
145
|
+
this.error = "Access denied - check credentials and bucket permissions";
|
|
146
|
+
}
|
|
147
|
+
return error;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get instructions describing this S3 filesystem.
|
|
151
|
+
* Used by agents to understand storage semantics.
|
|
152
|
+
*/
|
|
153
|
+
getInstructions() {
|
|
154
|
+
const providerName = this.displayName || "S3";
|
|
155
|
+
const access = this.readOnly ? "Read-only" : "Persistent";
|
|
156
|
+
return `${providerName} storage in bucket "${this.bucket}". ${access} storage - files are retained across sessions.`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Detect the appropriate icon based on the S3 endpoint.
|
|
160
|
+
*/
|
|
161
|
+
detectIconFromEndpoint(endpoint) {
|
|
162
|
+
if (!endpoint) {
|
|
163
|
+
return "aws-s3";
|
|
164
|
+
}
|
|
165
|
+
let hostname;
|
|
166
|
+
try {
|
|
167
|
+
const url = new URL(endpoint);
|
|
168
|
+
hostname = url.hostname.toLowerCase();
|
|
169
|
+
} catch {
|
|
170
|
+
hostname = endpoint.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
if (hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com") || hostname.endsWith(".cloudflare.com")) {
|
|
173
|
+
return "r2";
|
|
174
|
+
}
|
|
175
|
+
if (hostname === "storage.googleapis.com" || hostname.endsWith(".storage.googleapis.com") || hostname.endsWith(".googleapis.com")) {
|
|
176
|
+
return "gcs";
|
|
177
|
+
}
|
|
178
|
+
if (hostname === "blob.core.windows.net" || hostname.endsWith(".blob.core.windows.net") || hostname.endsWith(".azure.com")) {
|
|
179
|
+
return "azure";
|
|
180
|
+
}
|
|
181
|
+
if (hostname.includes("minio")) {
|
|
182
|
+
return "minio";
|
|
183
|
+
}
|
|
184
|
+
return "s3";
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get a user-friendly display name based on the icon/provider.
|
|
188
|
+
*/
|
|
189
|
+
getDefaultDisplayName(icon) {
|
|
190
|
+
switch (icon) {
|
|
191
|
+
case "aws-s3":
|
|
192
|
+
return "AWS S3";
|
|
193
|
+
case "r2":
|
|
194
|
+
case "cloudflare":
|
|
195
|
+
case "cloudflare-r2":
|
|
196
|
+
return "Cloudflare R2";
|
|
197
|
+
case "gcs":
|
|
198
|
+
case "google-cloud":
|
|
199
|
+
case "google-cloud-storage":
|
|
200
|
+
return "Google Cloud Storage";
|
|
201
|
+
case "azure":
|
|
202
|
+
case "azure-blob":
|
|
203
|
+
return "Azure Blob";
|
|
204
|
+
case "minio":
|
|
205
|
+
return "MinIO";
|
|
206
|
+
case "s3":
|
|
207
|
+
return "S3";
|
|
208
|
+
default:
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
getClient() {
|
|
213
|
+
if (this._client) return this._client;
|
|
214
|
+
const hasCredentials = this.accessKeyId && this.secretAccessKey;
|
|
215
|
+
this._client = new clientS3.S3Client({
|
|
216
|
+
region: this.region,
|
|
217
|
+
credentials: hasCredentials ? {
|
|
218
|
+
accessKeyId: this.accessKeyId,
|
|
219
|
+
secretAccessKey: this.secretAccessKey
|
|
220
|
+
} : (
|
|
221
|
+
// Anonymous access for public buckets - use empty credentials
|
|
222
|
+
// to prevent SDK from trying to find credentials elsewhere
|
|
223
|
+
{ accessKeyId: "", secretAccessKey: "" }
|
|
224
|
+
),
|
|
225
|
+
endpoint: this.endpoint,
|
|
226
|
+
forcePathStyle: this.forcePathStyle,
|
|
227
|
+
// Skip signing for anonymous access (public buckets).
|
|
228
|
+
// No-op signer passes the request through unsigned. Uses `any` because
|
|
229
|
+
// the correct type (HttpRequest from @smithy/types) is not a direct dependency.
|
|
230
|
+
...hasCredentials ? {} : { signer: { sign: async (request) => request } }
|
|
231
|
+
});
|
|
232
|
+
return this._client;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Ensure the filesystem is initialized and return the S3 client.
|
|
236
|
+
* Uses base class ensureReady() for status management, then returns client.
|
|
237
|
+
*/
|
|
238
|
+
async getReadyClient() {
|
|
239
|
+
await this.ensureReady();
|
|
240
|
+
return this.getClient();
|
|
241
|
+
}
|
|
242
|
+
toKey(path) {
|
|
243
|
+
const cleanPath = path.replace(/^\/+/, "");
|
|
244
|
+
return this.prefix + cleanPath;
|
|
245
|
+
}
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// File Operations
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
async readFile(path, options) {
|
|
250
|
+
const client = await this.getReadyClient();
|
|
251
|
+
try {
|
|
252
|
+
const response = await client.send(
|
|
253
|
+
new clientS3.GetObjectCommand({
|
|
254
|
+
Bucket: this.bucket,
|
|
255
|
+
Key: this.toKey(path)
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
const body = await response.Body?.transformToByteArray();
|
|
259
|
+
if (!body) throw new workspace.FileNotFoundError(path);
|
|
260
|
+
const buffer = Buffer.from(body);
|
|
261
|
+
if (options?.encoding) {
|
|
262
|
+
return buffer.toString(options.encoding);
|
|
263
|
+
}
|
|
264
|
+
return buffer;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (isNotFoundError(error)) {
|
|
267
|
+
throw new workspace.FileNotFoundError(path);
|
|
268
|
+
}
|
|
269
|
+
throw this.handleError(error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async writeFile(path, content, _options) {
|
|
273
|
+
const client = await this.getReadyClient();
|
|
274
|
+
const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
|
|
275
|
+
const contentType = getMimeType(path);
|
|
276
|
+
await client.send(
|
|
277
|
+
new clientS3.PutObjectCommand({
|
|
278
|
+
Bucket: this.bucket,
|
|
279
|
+
Key: this.toKey(path),
|
|
280
|
+
Body: body,
|
|
281
|
+
ContentType: contentType
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
async appendFile(path, content) {
|
|
286
|
+
let existing = "";
|
|
287
|
+
try {
|
|
288
|
+
existing = await this.readFile(path, { encoding: "utf-8" });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error instanceof workspace.FileNotFoundError) ; else {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
|
|
295
|
+
await this.writeFile(path, existing + appendContent);
|
|
296
|
+
}
|
|
297
|
+
async deleteFile(path, options) {
|
|
298
|
+
const isDir = await this.isDirectory(path);
|
|
299
|
+
if (isDir) {
|
|
300
|
+
await this.rmdir(path, { recursive: true, force: options?.force });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const client = await this.getReadyClient();
|
|
304
|
+
try {
|
|
305
|
+
await client.send(
|
|
306
|
+
new clientS3.DeleteObjectCommand({
|
|
307
|
+
Bucket: this.bucket,
|
|
308
|
+
Key: this.toKey(path)
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (options?.force) return;
|
|
313
|
+
if (isNotFoundError(error)) {
|
|
314
|
+
throw new workspace.FileNotFoundError(path);
|
|
315
|
+
}
|
|
316
|
+
throw this.handleError(error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async copyFile(src, dest, _options) {
|
|
320
|
+
const client = await this.getReadyClient();
|
|
321
|
+
try {
|
|
322
|
+
await client.send(
|
|
323
|
+
new clientS3.CopyObjectCommand({
|
|
324
|
+
Bucket: this.bucket,
|
|
325
|
+
CopySource: `${this.bucket}/${encodeURIComponent(this.toKey(src)).replace(/%2F/g, "/")}`,
|
|
326
|
+
Key: this.toKey(dest)
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (isNotFoundError(error)) {
|
|
331
|
+
throw new workspace.FileNotFoundError(src);
|
|
332
|
+
}
|
|
333
|
+
throw this.handleError(error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async moveFile(src, dest, options) {
|
|
337
|
+
await this.copyFile(src, dest, options);
|
|
338
|
+
await this.deleteFile(src, { force: true });
|
|
339
|
+
}
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Directory Operations
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
async mkdir(_path, _options) {
|
|
344
|
+
}
|
|
345
|
+
async rmdir(path, options) {
|
|
346
|
+
if (!options?.recursive) {
|
|
347
|
+
const entries = await this.readdir(path);
|
|
348
|
+
if (entries.length > 0) {
|
|
349
|
+
throw new Error(`Directory not empty: ${path}`);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const client = await this.getReadyClient();
|
|
354
|
+
const prefix = this.toKey(path).replace(/\/$/, "") + "/";
|
|
355
|
+
let continuationToken;
|
|
356
|
+
do {
|
|
357
|
+
const listResponse = await client.send(
|
|
358
|
+
new clientS3.ListObjectsV2Command({
|
|
359
|
+
Bucket: this.bucket,
|
|
360
|
+
Prefix: prefix,
|
|
361
|
+
ContinuationToken: continuationToken
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
|
365
|
+
const deleteResponse = await client.send(
|
|
366
|
+
new clientS3.DeleteObjectsCommand({
|
|
367
|
+
Bucket: this.bucket,
|
|
368
|
+
Delete: {
|
|
369
|
+
Objects: listResponse.Contents.filter((obj) => !!obj.Key).map((obj) => ({
|
|
370
|
+
Key: obj.Key
|
|
371
|
+
}))
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
|
+
if (deleteResponse.Errors && deleteResponse.Errors.length > 0) {
|
|
376
|
+
throw new Error(`Failed to delete ${deleteResponse.Errors.length} object(s) in ${path}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
continuationToken = listResponse.NextContinuationToken;
|
|
380
|
+
} while (continuationToken);
|
|
381
|
+
}
|
|
382
|
+
async readdir(path, options) {
|
|
383
|
+
const client = await this.getReadyClient();
|
|
384
|
+
const prefix = this.toKey(path).replace(/\/$/, "");
|
|
385
|
+
const searchPrefix = prefix ? prefix + "/" : "";
|
|
386
|
+
const entries = [];
|
|
387
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
388
|
+
let continuationToken;
|
|
389
|
+
do {
|
|
390
|
+
const response = await client.send(
|
|
391
|
+
new clientS3.ListObjectsV2Command({
|
|
392
|
+
Bucket: this.bucket,
|
|
393
|
+
Prefix: searchPrefix,
|
|
394
|
+
Delimiter: options?.recursive ? void 0 : "/",
|
|
395
|
+
ContinuationToken: continuationToken
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
if (response.Contents) {
|
|
399
|
+
for (const obj of response.Contents) {
|
|
400
|
+
const key = obj.Key;
|
|
401
|
+
if (!key || key === searchPrefix) continue;
|
|
402
|
+
const relativePath = key.slice(searchPrefix.length);
|
|
403
|
+
if (!relativePath) continue;
|
|
404
|
+
if (relativePath.endsWith("/")) {
|
|
405
|
+
const dirName = relativePath.slice(0, -1);
|
|
406
|
+
if (!seenDirs.has(dirName)) {
|
|
407
|
+
seenDirs.add(dirName);
|
|
408
|
+
entries.push({ name: dirName, type: "directory" });
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const name = options?.recursive ? relativePath : relativePath.split("/")[0];
|
|
413
|
+
if (!name) continue;
|
|
414
|
+
if (options?.extension) {
|
|
415
|
+
const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];
|
|
416
|
+
if (!extensions.some((ext) => name.endsWith(ext))) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
entries.push({
|
|
421
|
+
name,
|
|
422
|
+
type: "file",
|
|
423
|
+
size: obj.Size
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (response.CommonPrefixes) {
|
|
428
|
+
for (const prefixObj of response.CommonPrefixes) {
|
|
429
|
+
if (!prefixObj.Prefix) continue;
|
|
430
|
+
const dirName = prefixObj.Prefix.slice(searchPrefix.length).replace(/\/$/, "");
|
|
431
|
+
if (dirName && !seenDirs.has(dirName)) {
|
|
432
|
+
seenDirs.add(dirName);
|
|
433
|
+
entries.push({ name: dirName, type: "directory" });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
continuationToken = response.NextContinuationToken;
|
|
438
|
+
} while (continuationToken);
|
|
439
|
+
return entries;
|
|
440
|
+
}
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Path Operations
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
async exists(path) {
|
|
445
|
+
const key = this.toKey(path);
|
|
446
|
+
if (!key) return true;
|
|
447
|
+
const client = await this.getReadyClient();
|
|
448
|
+
try {
|
|
449
|
+
await client.send(
|
|
450
|
+
new clientS3.HeadObjectCommand({
|
|
451
|
+
Bucket: this.bucket,
|
|
452
|
+
Key: key
|
|
453
|
+
})
|
|
454
|
+
);
|
|
455
|
+
return true;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (!isNotFoundError(error)) throw this.handleError(error);
|
|
458
|
+
}
|
|
459
|
+
const response = await client.send(
|
|
460
|
+
new clientS3.ListObjectsV2Command({
|
|
461
|
+
Bucket: this.bucket,
|
|
462
|
+
Prefix: key.replace(/\/$/, "") + "/",
|
|
463
|
+
MaxKeys: 1
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
return (response.Contents?.length ?? 0) > 0;
|
|
467
|
+
}
|
|
468
|
+
async stat(path) {
|
|
469
|
+
const key = this.toKey(path);
|
|
470
|
+
if (!key) {
|
|
471
|
+
return {
|
|
472
|
+
name: "",
|
|
473
|
+
path,
|
|
474
|
+
type: "directory",
|
|
475
|
+
size: 0,
|
|
476
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
477
|
+
modifiedAt: /* @__PURE__ */ new Date()
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const client = await this.getReadyClient();
|
|
481
|
+
try {
|
|
482
|
+
const response = await client.send(
|
|
483
|
+
new clientS3.HeadObjectCommand({
|
|
484
|
+
Bucket: this.bucket,
|
|
485
|
+
Key: key
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
const name = path.split("/").pop() ?? "";
|
|
489
|
+
return {
|
|
490
|
+
name,
|
|
491
|
+
path,
|
|
492
|
+
type: "file",
|
|
493
|
+
size: response.ContentLength ?? 0,
|
|
494
|
+
createdAt: response.LastModified ?? /* @__PURE__ */ new Date(),
|
|
495
|
+
modifiedAt: response.LastModified ?? /* @__PURE__ */ new Date()
|
|
496
|
+
};
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (!isNotFoundError(error)) throw this.handleError(error);
|
|
499
|
+
const isDir = await this.isDirectory(path);
|
|
500
|
+
if (isDir) {
|
|
501
|
+
const name = path.split("/").filter(Boolean).pop() ?? "";
|
|
502
|
+
return {
|
|
503
|
+
name,
|
|
504
|
+
path,
|
|
505
|
+
type: "directory",
|
|
506
|
+
size: 0,
|
|
507
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
508
|
+
modifiedAt: /* @__PURE__ */ new Date()
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
throw new workspace.FileNotFoundError(path);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async isFile(path) {
|
|
515
|
+
const key = this.toKey(path);
|
|
516
|
+
if (!key) return false;
|
|
517
|
+
const client = await this.getReadyClient();
|
|
518
|
+
try {
|
|
519
|
+
await client.send(
|
|
520
|
+
new clientS3.HeadObjectCommand({
|
|
521
|
+
Bucket: this.bucket,
|
|
522
|
+
Key: key
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
return true;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (!isNotFoundError(error)) throw this.handleError(error);
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async isDirectory(path) {
|
|
532
|
+
const key = this.toKey(path);
|
|
533
|
+
if (!key) return true;
|
|
534
|
+
const client = await this.getReadyClient();
|
|
535
|
+
const response = await client.send(
|
|
536
|
+
new clientS3.ListObjectsV2Command({
|
|
537
|
+
Bucket: this.bucket,
|
|
538
|
+
Prefix: key.replace(/\/$/, "") + "/",
|
|
539
|
+
MaxKeys: 1
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
return (response.Contents?.length ?? 0) > 0;
|
|
543
|
+
}
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Lifecycle (overrides base class protected methods)
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
/**
|
|
548
|
+
* Initialize the S3 client.
|
|
549
|
+
* Status management is handled by the base class.
|
|
550
|
+
*/
|
|
551
|
+
async init() {
|
|
552
|
+
const client = this.getClient();
|
|
553
|
+
try {
|
|
554
|
+
await client.send(new clientS3.HeadBucketCommand({ Bucket: this.bucket }));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
const statusCode = error.$metadata?.httpStatusCode;
|
|
557
|
+
const createError = (message2) => {
|
|
558
|
+
const err = new Error(message2);
|
|
559
|
+
if (statusCode) err.status = statusCode;
|
|
560
|
+
return err;
|
|
561
|
+
};
|
|
562
|
+
if (isAccessDeniedError(error)) {
|
|
563
|
+
throw createError(`Access denied to bucket "${this.bucket}" - check credentials and permissions`);
|
|
564
|
+
}
|
|
565
|
+
if (isNotFoundError(error)) {
|
|
566
|
+
throw createError(`Bucket "${this.bucket}" not found`);
|
|
567
|
+
}
|
|
568
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
569
|
+
if (statusCode) {
|
|
570
|
+
throw createError(`Failed to access bucket "${this.bucket}" (HTTP ${statusCode}): ${message}`);
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Clean up the S3 client.
|
|
577
|
+
* Status management is handled by the base class.
|
|
578
|
+
*/
|
|
579
|
+
async destroy() {
|
|
580
|
+
this._client = null;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
exports.S3Filesystem = S3Filesystem;
|
|
585
|
+
//# sourceMappingURL=index.cjs.map
|
|
586
|
+
//# sourceMappingURL=index.cjs.map
|