@mastra/files-sdk 0.3.0-alpha.0 → 0.3.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/dist/index.js CHANGED
@@ -1,424 +1,455 @@
1
- import { MastraFilesystem, FileNotFoundError, FileExistsError, DirectoryNotEmptyError, WorkspaceReadOnlyError } from '@mastra/core/workspace';
2
-
3
- // src/filesystem/index.ts
1
+ import { DirectoryNotEmptyError, FileExistsError, FileNotFoundError, MastraFilesystem, WorkspaceReadOnlyError } from "@mastra/core/workspace";
2
+ //#region src/filesystem/index.ts
3
+ /**
4
+ * Normalize a POSIX-style path to an object-storage key.
5
+ * Strips leading slashes, resolves `.`/`./` to empty string.
6
+ */
4
7
  function toKey(path) {
5
- let start = 0;
6
- while (start < path.length && path.charCodeAt(start) === 47) start++;
7
- let end = path.length;
8
- while (end > start && path.charCodeAt(end - 1) === 47) end--;
9
- const key = path.slice(start, end);
10
- if (key === "." || key === "./") return "";
11
- return key;
8
+ let start = 0;
9
+ while (start < path.length && path.charCodeAt(start) === 47) start++;
10
+ let end = path.length;
11
+ while (end > start && path.charCodeAt(end - 1) === 47) end--;
12
+ const key = path.slice(start, end);
13
+ if (key === "." || key === "./") return "";
14
+ return key;
12
15
  }
16
+ /**
17
+ * Extract the basename (last path segment) from a key.
18
+ */
13
19
  function basename(key) {
14
- const idx = key.lastIndexOf("/");
15
- return idx === -1 ? key : key.slice(idx + 1);
20
+ const idx = key.lastIndexOf("/");
21
+ return idx === -1 ? key : key.slice(idx + 1);
16
22
  }
23
+ /**
24
+ * Walk the FilesSDK error code chain.
25
+ *
26
+ * FilesSDK frequently wraps an inner `NotFound` / `Unauthorized` error in an
27
+ * outer `Provider` error (`error.cause` carries the original). Some adapters
28
+ * may wrap multiple levels deep, so we walk the cause chain and return any
29
+ * matching code found along the way.
30
+ */
17
31
  function hasFilesSDKCode(err, code, depth = 0) {
18
- if (depth > 5 || !err || typeof err !== "object") return false;
19
- const e = err;
20
- if (e.code === code) return true;
21
- if (e.cause) return hasFilesSDKCode(e.cause, code, depth + 1);
22
- return false;
32
+ if (depth > 5 || !err || typeof err !== "object") return false;
33
+ const e = err;
34
+ if (e.code === code) return true;
35
+ if (e.cause) return hasFilesSDKCode(e.cause, code, depth + 1);
36
+ return false;
23
37
  }
24
38
  function isNotFoundError(err) {
25
- return hasFilesSDKCode(err, "NotFound");
39
+ return hasFilesSDKCode(err, "NotFound");
26
40
  }
27
41
  function isUnauthorizedError(err) {
28
- return hasFilesSDKCode(err, "Unauthorized");
42
+ return hasFilesSDKCode(err, "Unauthorized");
29
43
  }
30
44
  function generateId() {
31
- return `files-sdk-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
45
+ return `files-sdk-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
32
46
  }
47
+ /**
48
+ * Convert FileContent (string | Buffer | Uint8Array) to a body acceptable by files-sdk.
49
+ */
33
50
  function toBody(content) {
34
- if (typeof content === "string") return content;
35
- if (Buffer.isBuffer(content)) return new Uint8Array(content);
36
- return content;
51
+ if (typeof content === "string") return content;
52
+ if (Buffer.isBuffer(content)) return new Uint8Array(content);
53
+ return content;
37
54
  }
38
- var MIME_TYPES = {
39
- ".txt": "text/plain",
40
- ".md": "text/markdown",
41
- ".markdown": "text/markdown",
42
- ".html": "text/html",
43
- ".htm": "text/html",
44
- ".css": "text/css",
45
- ".csv": "text/csv",
46
- ".xml": "text/xml",
47
- ".js": "text/javascript",
48
- ".mjs": "text/javascript",
49
- ".ts": "text/typescript",
50
- ".tsx": "text/typescript",
51
- ".jsx": "text/javascript",
52
- ".json": "application/json",
53
- ".yaml": "text/yaml",
54
- ".yml": "text/yaml",
55
- ".py": "text/x-python",
56
- ".rb": "text/x-ruby",
57
- ".sh": "text/x-shellscript",
58
- ".bash": "text/x-shellscript",
59
- ".png": "image/png",
60
- ".jpg": "image/jpeg",
61
- ".jpeg": "image/jpeg",
62
- ".gif": "image/gif",
63
- ".svg": "image/svg+xml",
64
- ".webp": "image/webp",
65
- ".ico": "image/x-icon",
66
- ".pdf": "application/pdf",
67
- ".zip": "application/zip",
68
- ".gz": "application/gzip",
69
- ".tar": "application/x-tar"
55
+ /**
56
+ * Infer MIME type from a file path extension.
57
+ */
58
+ const MIME_TYPES = {
59
+ ".txt": "text/plain",
60
+ ".md": "text/markdown",
61
+ ".markdown": "text/markdown",
62
+ ".html": "text/html",
63
+ ".htm": "text/html",
64
+ ".css": "text/css",
65
+ ".csv": "text/csv",
66
+ ".xml": "text/xml",
67
+ ".js": "text/javascript",
68
+ ".mjs": "text/javascript",
69
+ ".ts": "text/typescript",
70
+ ".tsx": "text/typescript",
71
+ ".jsx": "text/javascript",
72
+ ".json": "application/json",
73
+ ".yaml": "text/yaml",
74
+ ".yml": "text/yaml",
75
+ ".py": "text/x-python",
76
+ ".rb": "text/x-ruby",
77
+ ".sh": "text/x-shellscript",
78
+ ".bash": "text/x-shellscript",
79
+ ".png": "image/png",
80
+ ".jpg": "image/jpeg",
81
+ ".jpeg": "image/jpeg",
82
+ ".gif": "image/gif",
83
+ ".svg": "image/svg+xml",
84
+ ".webp": "image/webp",
85
+ ".ico": "image/x-icon",
86
+ ".pdf": "application/pdf",
87
+ ".zip": "application/zip",
88
+ ".gz": "application/gzip",
89
+ ".tar": "application/x-tar"
70
90
  };
71
91
  function getMimeType(path) {
72
- const dot = path.lastIndexOf(".");
73
- if (dot === -1) return "application/octet-stream";
74
- return MIME_TYPES[path.slice(dot).toLowerCase()] ?? "application/octet-stream";
92
+ const dot = path.lastIndexOf(".");
93
+ if (dot === -1) return "application/octet-stream";
94
+ return MIME_TYPES[path.slice(dot).toLowerCase()] ?? "application/octet-stream";
75
95
  }
96
+ /**
97
+ * Workspace filesystem adapter backed by [FilesSDK](https://files-sdk.dev).
98
+ *
99
+ * Accepts a pre-configured `Files` instance so users choose their own adapter
100
+ * (S3, R2, GCS, Azure, local fs, etc.) and this class bridges it to the
101
+ * Mastra `WorkspaceFilesystem` interface.
102
+ *
103
+ * Object-storage semantics are bridged to the POSIX-like interface:
104
+ * - `mkdir` is a no-op (directories don't exist in object storage)
105
+ * - `readdir` uses `list()` with prefix filtering to synthesize directory entries
106
+ * - `rmdir` lists all keys under a prefix and batch-deletes them
107
+ */
76
108
  var FilesSDKFilesystem = class extends MastraFilesystem {
77
- id;
78
- name = "FilesSDKFilesystem";
79
- provider = "files-sdk";
80
- status = "pending";
81
- readOnly;
82
- icon;
83
- displayName;
84
- description;
85
- _files;
86
- constructor(options) {
87
- super({ name: "FilesSDKFilesystem", ...options });
88
- this._files = options.files;
89
- this.id = options.id ?? generateId();
90
- this.readOnly = options.readOnly;
91
- this.icon = options.icon;
92
- this.displayName = options.displayName;
93
- this.description = options.description;
94
- }
95
- /** The underlying FilesSDK instance, for escape-hatch access. */
96
- get files() {
97
- return this._files;
98
- }
99
- // ---------------------------------------------------------------------------
100
- // Lifecycle
101
- // ---------------------------------------------------------------------------
102
- async init() {
103
- try {
104
- await this._files.list({ limit: 1 });
105
- } catch (err) {
106
- if (isUnauthorizedError(err)) {
107
- throw new Error("Access denied \u2014 check credentials and storage permissions");
108
- }
109
- throw err;
110
- }
111
- }
112
- // destroy() — default no-op is fine; FilesSDK has no explicit teardown.
113
- getInfo() {
114
- return {
115
- id: this.id,
116
- name: this.name,
117
- provider: this.provider,
118
- status: this.status,
119
- error: this.error,
120
- readOnly: this.readOnly,
121
- icon: this.icon,
122
- metadata: {
123
- adapter: this._files.adapter?.name ?? "unknown"
124
- }
125
- };
126
- }
127
- getInstructions() {
128
- const adapterName = this._files.adapter?.name ?? "unknown";
129
- const parts = [`Unified storage via FilesSDK (${adapterName} adapter).`];
130
- if (this.readOnly) parts.push("Mounted read-only.");
131
- parts.push("Persistent storage \u2014 files are retained across sessions.");
132
- return parts.join(" ");
133
- }
134
- // ---------------------------------------------------------------------------
135
- // File operations
136
- // ---------------------------------------------------------------------------
137
- async readFile(path, options) {
138
- await this.ensureReady();
139
- const key = toKey(path);
140
- try {
141
- const file = await this._files.download(key);
142
- const buf = Buffer.from(await file.arrayBuffer());
143
- if (options?.encoding) return buf.toString(options.encoding);
144
- return buf;
145
- } catch (err) {
146
- if (isNotFoundError(err)) throw new FileNotFoundError(path);
147
- throw err;
148
- }
149
- }
150
- async writeFile(path, content, options) {
151
- await this.ensureReady();
152
- this.assertWritable("writeFile");
153
- const key = toKey(path);
154
- if (options?.overwrite === false && await this.isFile(key)) {
155
- throw new FileExistsError(path);
156
- }
157
- const body = toBody(content);
158
- await this._files.upload(key, body, {
159
- contentType: options?.mimeType ?? getMimeType(path)
160
- });
161
- }
162
- /**
163
- * Append content to a file.
164
- *
165
- * **Not atomic.** Object storage has no native append, so this is implemented
166
- * as a read-modify-write: the existing object is downloaded, the new content
167
- * is concatenated, and the result is uploaded as a new object. Concurrent
168
- * appends to the same key may overwrite each other. This limitation is
169
- * inherent to object storage, not specific to FilesSDK, and matches the
170
- * behavior of the sibling S3, GCS, and Azure workspace providers.
171
- */
172
- async appendFile(path, content) {
173
- await this.ensureReady();
174
- this.assertWritable("appendFile");
175
- const key = toKey(path);
176
- let existing = Buffer.alloc(0);
177
- try {
178
- const file = await this._files.download(key);
179
- existing = Buffer.from(await file.arrayBuffer());
180
- } catch (err) {
181
- if (!isNotFoundError(err)) throw err;
182
- }
183
- const append = typeof content === "string" ? Buffer.from(content) : toBody(content);
184
- const merged = Buffer.concat([existing, Buffer.isBuffer(append) ? append : Buffer.from(append)]);
185
- await this._files.upload(key, new Uint8Array(merged), {
186
- contentType: getMimeType(path)
187
- });
188
- }
189
- async deleteFile(path, options) {
190
- await this.ensureReady();
191
- this.assertWritable("deleteFile");
192
- const key = toKey(path);
193
- if (await this.isDirectory(key)) {
194
- await this.rmdir(path, { recursive: true, force: options?.force });
195
- return;
196
- }
197
- if (!options?.force && !await this.isFile(key)) {
198
- throw new FileNotFoundError(path);
199
- }
200
- try {
201
- await this._files.delete(key);
202
- } catch (err) {
203
- if (options?.force) return;
204
- if (isNotFoundError(err)) throw new FileNotFoundError(path);
205
- throw err;
206
- }
207
- }
208
- async copyFile(src, dest, options) {
209
- await this.ensureReady();
210
- this.assertWritable("copyFile");
211
- const fromKey = toKey(src);
212
- const toKey_ = toKey(dest);
213
- if (options?.overwrite === false && await this.isFile(toKey_)) {
214
- throw new FileExistsError(dest);
215
- }
216
- try {
217
- await this._files.copy(fromKey, toKey_);
218
- } catch (err) {
219
- if (isNotFoundError(err)) throw new FileNotFoundError(src);
220
- throw err;
221
- }
222
- }
223
- async moveFile(src, dest, options) {
224
- await this.copyFile(src, dest, options);
225
- await this.deleteFile(src, { force: true });
226
- }
227
- // ---------------------------------------------------------------------------
228
- // Directory operations
229
- // ---------------------------------------------------------------------------
230
- async mkdir(_path, _options) {
231
- await this.ensureReady();
232
- this.assertWritable("mkdir");
233
- }
234
- async rmdir(path, options) {
235
- await this.ensureReady();
236
- this.assertWritable("rmdir");
237
- const key = toKey(path);
238
- const prefix = key ? `${key}/` : "";
239
- const allKeys = [];
240
- let cursor;
241
- do {
242
- const result = await this._files.list({ prefix, cursor, limit: 1e3 });
243
- for (const item of result.items) {
244
- allKeys.push(item.key);
245
- }
246
- cursor = result.cursor;
247
- } while (cursor);
248
- if (allKeys.length === 0) return;
249
- if (!options?.recursive) {
250
- throw new DirectoryNotEmptyError(path);
251
- }
252
- await this._files.delete(allKeys);
253
- }
254
- async readdir(path, options) {
255
- await this.ensureReady();
256
- const key = toKey(path);
257
- const prefix = key ? `${key}/` : "";
258
- const entries = [];
259
- const seenDirs = /* @__PURE__ */ new Set();
260
- let cursor;
261
- const maxDepth = options?.maxDepth;
262
- const recursive = options?.recursive ?? false;
263
- const extensions = options?.extension ? Array.isArray(options.extension) ? options.extension : [options.extension] : void 0;
264
- do {
265
- const result = await this._files.list({ prefix, cursor, limit: 1e3 });
266
- for (const item of result.items) {
267
- const relativePath = prefix ? item.key.slice(prefix.length) : item.key;
268
- if (!relativePath) continue;
269
- const segments = relativePath.split("/");
270
- if (segments.length === 1) {
271
- const name = segments[0];
272
- if (extensions) {
273
- const ext = name.lastIndexOf(".") !== -1 ? name.slice(name.lastIndexOf(".")) : "";
274
- if (!extensions.includes(ext)) continue;
275
- }
276
- entries.push({
277
- name,
278
- type: "file",
279
- size: item.size
280
- });
281
- } else if (!recursive) {
282
- const dirName = segments[0];
283
- if (!seenDirs.has(dirName)) {
284
- seenDirs.add(dirName);
285
- entries.push({
286
- name: dirName,
287
- type: "directory"
288
- });
289
- }
290
- } else {
291
- for (let i = 1; i < segments.length; i++) {
292
- const dirPath = segments.slice(0, i).join("/");
293
- const dirDepth = i;
294
- if (maxDepth !== void 0 && dirDepth > maxDepth) continue;
295
- if (!seenDirs.has(dirPath)) {
296
- seenDirs.add(dirPath);
297
- entries.push({
298
- name: dirPath,
299
- type: "directory"
300
- });
301
- }
302
- }
303
- if (maxDepth !== void 0 && segments.length > maxDepth) continue;
304
- const name = relativePath;
305
- if (extensions) {
306
- const ext = name.lastIndexOf(".") !== -1 ? name.slice(name.lastIndexOf(".")) : "";
307
- if (!extensions.includes(ext)) continue;
308
- }
309
- entries.push({
310
- name,
311
- type: "file",
312
- size: item.size
313
- });
314
- }
315
- }
316
- cursor = result.cursor;
317
- } while (cursor);
318
- return entries;
319
- }
320
- // ---------------------------------------------------------------------------
321
- // Path operations
322
- // ---------------------------------------------------------------------------
323
- async exists(path) {
324
- await this.ensureReady();
325
- const key = toKey(path);
326
- if (!key) return true;
327
- if (await this.isDirectory(key)) return true;
328
- return this.isFile(key);
329
- }
330
- async stat(path) {
331
- await this.ensureReady();
332
- const key = toKey(path);
333
- if (!key) {
334
- const now = /* @__PURE__ */ new Date();
335
- return {
336
- name: "/",
337
- path: "/",
338
- type: "directory",
339
- size: 0,
340
- createdAt: now,
341
- modifiedAt: now
342
- };
343
- }
344
- try {
345
- const file = await this._files.head(key);
346
- return this.storedFileToStat(file, path);
347
- } catch (err) {
348
- if (!isNotFoundError(err)) throw err;
349
- }
350
- if (await this.isDirectory(key)) {
351
- const now = /* @__PURE__ */ new Date();
352
- return {
353
- name: basename(key),
354
- path,
355
- type: "directory",
356
- size: 0,
357
- createdAt: now,
358
- modifiedAt: now
359
- };
360
- }
361
- throw new FileNotFoundError(path);
362
- }
363
- // ---------------------------------------------------------------------------
364
- // Private helpers
365
- // ---------------------------------------------------------------------------
366
- assertWritable(operation) {
367
- if (this.readOnly) {
368
- throw new WorkspaceReadOnlyError(operation);
369
- }
370
- }
371
- /** Check if a key prefix has any children (i.e. acts like a directory). */
372
- async isDirectory(key) {
373
- if (!key) return true;
374
- const prefix = `${key}/`;
375
- const result = await this._files.list({ prefix, limit: 1 });
376
- return result.items.length > 0;
377
- }
378
- /**
379
- * Check whether `key` refers to a real stored file (not an empty leftover
380
- * directory). Uses prefix listing constrained to the exact key, which only
381
- * matches actually-stored objects across all adapters.
382
- */
383
- async isFile(key) {
384
- if (!key) return false;
385
- const result = await this._files.list({ prefix: key, limit: 10 });
386
- return result.items.some((item) => item.key === key);
387
- }
388
- /** Convert a FilesSDK StoredFile to a Mastra FileStat. */
389
- storedFileToStat(file, path) {
390
- return {
391
- name: basename(file.key ?? path),
392
- path,
393
- type: "file",
394
- size: file.size ?? 0,
395
- createdAt: file.lastModified ? new Date(file.lastModified) : /* @__PURE__ */ new Date(),
396
- modifiedAt: file.lastModified ? new Date(file.lastModified) : /* @__PURE__ */ new Date(),
397
- mimeType: file.type || void 0
398
- };
399
- }
109
+ id;
110
+ name = "FilesSDKFilesystem";
111
+ provider = "files-sdk";
112
+ status = "pending";
113
+ readOnly;
114
+ icon;
115
+ displayName;
116
+ description;
117
+ _files;
118
+ constructor(options) {
119
+ super({
120
+ name: "FilesSDKFilesystem",
121
+ ...options
122
+ });
123
+ this._files = options.files;
124
+ this.id = options.id ?? generateId();
125
+ this.readOnly = options.readOnly;
126
+ this.icon = options.icon;
127
+ this.displayName = options.displayName;
128
+ this.description = options.description;
129
+ }
130
+ /** The underlying FilesSDK instance, for escape-hatch access. */
131
+ get files() {
132
+ return this._files;
133
+ }
134
+ async init() {
135
+ try {
136
+ await this._files.list({ limit: 1 });
137
+ } catch (err) {
138
+ if (isUnauthorizedError(err)) throw new Error("Access denied — check credentials and storage permissions");
139
+ throw err;
140
+ }
141
+ }
142
+ getInfo() {
143
+ return {
144
+ id: this.id,
145
+ name: this.name,
146
+ provider: this.provider,
147
+ status: this.status,
148
+ error: this.error,
149
+ readOnly: this.readOnly,
150
+ icon: this.icon,
151
+ metadata: { adapter: this._files.adapter?.name ?? "unknown" }
152
+ };
153
+ }
154
+ getInstructions() {
155
+ const parts = [`Unified storage via FilesSDK (${this._files.adapter?.name ?? "unknown"} adapter).`];
156
+ if (this.readOnly) parts.push("Mounted read-only.");
157
+ parts.push("Persistent storage — files are retained across sessions.");
158
+ return parts.join(" ");
159
+ }
160
+ async readFile(path, options) {
161
+ await this.ensureReady();
162
+ const key = toKey(path);
163
+ try {
164
+ const file = await this._files.download(key);
165
+ const buf = Buffer.from(await file.arrayBuffer());
166
+ if (options?.encoding) return buf.toString(options.encoding);
167
+ return buf;
168
+ } catch (err) {
169
+ if (isNotFoundError(err)) throw new FileNotFoundError(path);
170
+ throw err;
171
+ }
172
+ }
173
+ async writeFile(path, content, options) {
174
+ await this.ensureReady();
175
+ this.assertWritable("writeFile");
176
+ const key = toKey(path);
177
+ if (options?.overwrite === false && await this.isFile(key)) throw new FileExistsError(path);
178
+ const body = toBody(content);
179
+ await this._files.upload(key, body, { contentType: options?.mimeType ?? getMimeType(path) });
180
+ }
181
+ /**
182
+ * Append content to a file.
183
+ *
184
+ * **Not atomic.** Object storage has no native append, so this is implemented
185
+ * as a read-modify-write: the existing object is downloaded, the new content
186
+ * is concatenated, and the result is uploaded as a new object. Concurrent
187
+ * appends to the same key may overwrite each other. This limitation is
188
+ * inherent to object storage, not specific to FilesSDK, and matches the
189
+ * behavior of the sibling S3, GCS, and Azure workspace providers.
190
+ */
191
+ async appendFile(path, content) {
192
+ await this.ensureReady();
193
+ this.assertWritable("appendFile");
194
+ const key = toKey(path);
195
+ let existing = Buffer.alloc(0);
196
+ try {
197
+ const file = await this._files.download(key);
198
+ existing = Buffer.from(await file.arrayBuffer());
199
+ } catch (err) {
200
+ if (!isNotFoundError(err)) throw err;
201
+ }
202
+ const append = typeof content === "string" ? Buffer.from(content) : toBody(content);
203
+ const merged = Buffer.concat([existing, Buffer.isBuffer(append) ? append : Buffer.from(append)]);
204
+ await this._files.upload(key, new Uint8Array(merged), { contentType: getMimeType(path) });
205
+ }
206
+ async deleteFile(path, options) {
207
+ await this.ensureReady();
208
+ this.assertWritable("deleteFile");
209
+ const key = toKey(path);
210
+ if (await this.isDirectory(key)) {
211
+ await this.rmdir(path, {
212
+ recursive: true,
213
+ force: options?.force
214
+ });
215
+ return;
216
+ }
217
+ if (!options?.force && !await this.isFile(key)) throw new FileNotFoundError(path);
218
+ try {
219
+ await this._files.delete(key);
220
+ } catch (err) {
221
+ if (options?.force) return;
222
+ if (isNotFoundError(err)) throw new FileNotFoundError(path);
223
+ throw err;
224
+ }
225
+ }
226
+ async copyFile(src, dest, options) {
227
+ await this.ensureReady();
228
+ this.assertWritable("copyFile");
229
+ const fromKey = toKey(src);
230
+ const toKey_ = toKey(dest);
231
+ if (options?.overwrite === false && await this.isFile(toKey_)) throw new FileExistsError(dest);
232
+ try {
233
+ await this._files.copy(fromKey, toKey_);
234
+ } catch (err) {
235
+ if (isNotFoundError(err)) throw new FileNotFoundError(src);
236
+ throw err;
237
+ }
238
+ }
239
+ async moveFile(src, dest, options) {
240
+ await this.copyFile(src, dest, options);
241
+ await this.deleteFile(src, { force: true });
242
+ }
243
+ async mkdir(_path, _options) {
244
+ await this.ensureReady();
245
+ this.assertWritable("mkdir");
246
+ }
247
+ async rmdir(path, options) {
248
+ await this.ensureReady();
249
+ this.assertWritable("rmdir");
250
+ const key = toKey(path);
251
+ const prefix = key ? `${key}/` : "";
252
+ const allKeys = [];
253
+ let cursor;
254
+ do {
255
+ const result = await this._files.list({
256
+ prefix,
257
+ cursor,
258
+ limit: 1e3
259
+ });
260
+ for (const item of result.items) allKeys.push(item.key);
261
+ cursor = result.cursor;
262
+ } while (cursor);
263
+ if (allKeys.length === 0) return;
264
+ if (!options?.recursive) throw new DirectoryNotEmptyError(path);
265
+ await this._files.delete(allKeys);
266
+ }
267
+ async readdir(path, options) {
268
+ await this.ensureReady();
269
+ const key = toKey(path);
270
+ const prefix = key ? `${key}/` : "";
271
+ const entries = [];
272
+ const seenDirs = /* @__PURE__ */ new Set();
273
+ let cursor;
274
+ const maxDepth = options?.maxDepth;
275
+ const recursive = options?.recursive ?? false;
276
+ const extensions = options?.extension ? Array.isArray(options.extension) ? options.extension : [options.extension] : void 0;
277
+ do {
278
+ const result = await this._files.list({
279
+ prefix,
280
+ cursor,
281
+ limit: 1e3
282
+ });
283
+ for (const item of result.items) {
284
+ const relativePath = prefix ? item.key.slice(prefix.length) : item.key;
285
+ if (!relativePath) continue;
286
+ const segments = relativePath.split("/");
287
+ if (segments.length === 1) {
288
+ const name = segments[0];
289
+ if (extensions) {
290
+ const ext = name.lastIndexOf(".") !== -1 ? name.slice(name.lastIndexOf(".")) : "";
291
+ if (!extensions.includes(ext)) continue;
292
+ }
293
+ entries.push({
294
+ name,
295
+ type: "file",
296
+ size: item.size
297
+ });
298
+ } else if (!recursive) {
299
+ const dirName = segments[0];
300
+ if (!seenDirs.has(dirName)) {
301
+ seenDirs.add(dirName);
302
+ entries.push({
303
+ name: dirName,
304
+ type: "directory"
305
+ });
306
+ }
307
+ } else {
308
+ for (let i = 1; i < segments.length; i++) {
309
+ const dirPath = segments.slice(0, i).join("/");
310
+ if (maxDepth !== void 0 && i > maxDepth) continue;
311
+ if (!seenDirs.has(dirPath)) {
312
+ seenDirs.add(dirPath);
313
+ entries.push({
314
+ name: dirPath,
315
+ type: "directory"
316
+ });
317
+ }
318
+ }
319
+ if (maxDepth !== void 0 && segments.length > maxDepth) continue;
320
+ const name = relativePath;
321
+ if (extensions) {
322
+ const ext = name.lastIndexOf(".") !== -1 ? name.slice(name.lastIndexOf(".")) : "";
323
+ if (!extensions.includes(ext)) continue;
324
+ }
325
+ entries.push({
326
+ name,
327
+ type: "file",
328
+ size: item.size
329
+ });
330
+ }
331
+ }
332
+ cursor = result.cursor;
333
+ } while (cursor);
334
+ return entries;
335
+ }
336
+ async exists(path) {
337
+ await this.ensureReady();
338
+ const key = toKey(path);
339
+ if (!key) return true;
340
+ if (await this.isDirectory(key)) return true;
341
+ return this.isFile(key);
342
+ }
343
+ async stat(path) {
344
+ await this.ensureReady();
345
+ const key = toKey(path);
346
+ if (!key) {
347
+ const now = /* @__PURE__ */ new Date();
348
+ return {
349
+ name: "/",
350
+ path: "/",
351
+ type: "directory",
352
+ size: 0,
353
+ createdAt: now,
354
+ modifiedAt: now
355
+ };
356
+ }
357
+ try {
358
+ const file = await this._files.head(key);
359
+ return this.storedFileToStat(file, path);
360
+ } catch (err) {
361
+ if (!isNotFoundError(err)) throw err;
362
+ }
363
+ if (await this.isDirectory(key)) {
364
+ const now = /* @__PURE__ */ new Date();
365
+ return {
366
+ name: basename(key),
367
+ path,
368
+ type: "directory",
369
+ size: 0,
370
+ createdAt: now,
371
+ modifiedAt: now
372
+ };
373
+ }
374
+ throw new FileNotFoundError(path);
375
+ }
376
+ assertWritable(operation) {
377
+ if (this.readOnly) throw new WorkspaceReadOnlyError(operation);
378
+ }
379
+ /** Check if a key prefix has any children (i.e. acts like a directory). */
380
+ async isDirectory(key) {
381
+ if (!key) return true;
382
+ const prefix = `${key}/`;
383
+ return (await this._files.list({
384
+ prefix,
385
+ limit: 1
386
+ })).items.length > 0;
387
+ }
388
+ /**
389
+ * Check whether `key` refers to a real stored file (not an empty leftover
390
+ * directory). Uses prefix listing constrained to the exact key, which only
391
+ * matches actually-stored objects across all adapters.
392
+ */
393
+ async isFile(key) {
394
+ if (!key) return false;
395
+ return (await this._files.list({
396
+ prefix: key,
397
+ limit: 10
398
+ })).items.some((item) => item.key === key);
399
+ }
400
+ /** Convert a FilesSDK StoredFile to a Mastra FileStat. */
401
+ storedFileToStat(file, path) {
402
+ return {
403
+ name: basename(file.key ?? path),
404
+ path,
405
+ type: "file",
406
+ size: file.size ?? 0,
407
+ createdAt: file.lastModified ? new Date(file.lastModified) : /* @__PURE__ */ new Date(),
408
+ modifiedAt: file.lastModified ? new Date(file.lastModified) : /* @__PURE__ */ new Date(),
409
+ mimeType: file.type || void 0
410
+ };
411
+ }
400
412
  };
401
-
402
- // src/provider.ts
403
- var filesSDKFilesystemProvider = {
404
- id: "files-sdk",
405
- name: "FilesSDK (Unified Storage)",
406
- description: "Unified storage via FilesSDK \u2014 supports S3, R2, GCS, Azure, Vercel Blob, local filesystem, and more",
407
- configSchema: {
408
- type: "object",
409
- required: ["files"],
410
- properties: {
411
- files: { type: "object", description: "Pre-configured FilesSDK Files instance" },
412
- id: { type: "string", description: "Unique filesystem ID" },
413
- displayName: { type: "string", description: "Human-friendly display name" },
414
- icon: { type: "string", description: "Icon identifier for UI" },
415
- description: { type: "string", description: "Description for UI" },
416
- readOnly: { type: "boolean", description: "Mount as read-only", default: false }
417
- }
418
- },
419
- createFilesystem: (config) => new FilesSDKFilesystem(config)
413
+ //#endregion
414
+ //#region src/provider.ts
415
+ const filesSDKFilesystemProvider = {
416
+ id: "files-sdk",
417
+ name: "FilesSDK (Unified Storage)",
418
+ description: "Unified storage via FilesSDK — supports S3, R2, GCS, Azure, Vercel Blob, local filesystem, and more",
419
+ configSchema: {
420
+ type: "object",
421
+ required: ["files"],
422
+ properties: {
423
+ files: {
424
+ type: "object",
425
+ description: "Pre-configured FilesSDK Files instance"
426
+ },
427
+ id: {
428
+ type: "string",
429
+ description: "Unique filesystem ID"
430
+ },
431
+ displayName: {
432
+ type: "string",
433
+ description: "Human-friendly display name"
434
+ },
435
+ icon: {
436
+ type: "string",
437
+ description: "Icon identifier for UI"
438
+ },
439
+ description: {
440
+ type: "string",
441
+ description: "Description for UI"
442
+ },
443
+ readOnly: {
444
+ type: "boolean",
445
+ description: "Mount as read-only",
446
+ default: false
447
+ }
448
+ }
449
+ },
450
+ createFilesystem: (config) => new FilesSDKFilesystem(config)
420
451
  };
421
-
452
+ //#endregion
422
453
  export { FilesSDKFilesystem, filesSDKFilesystemProvider };
423
- //# sourceMappingURL=index.js.map
454
+
424
455
  //# sourceMappingURL=index.js.map