@mastra/gcs 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,493 +1,527 @@
1
- import { Storage } from '@google-cloud/storage';
2
- import { MastraFilesystem, FileNotFoundError, FileExistsError } from '@mastra/core/workspace';
3
-
4
- // src/filesystem/index.ts
5
- var MIME_TYPES = {
6
- // Text
7
- ".txt": "text/plain",
8
- ".md": "text/markdown",
9
- ".markdown": "text/markdown",
10
- ".html": "text/html",
11
- ".htm": "text/html",
12
- ".css": "text/css",
13
- ".csv": "text/csv",
14
- ".xml": "text/xml",
15
- // Code
16
- ".js": "text/javascript",
17
- ".mjs": "text/javascript",
18
- ".ts": "text/typescript",
19
- ".tsx": "text/typescript",
20
- ".jsx": "text/javascript",
21
- ".json": "application/json",
22
- ".yaml": "text/yaml",
23
- ".yml": "text/yaml",
24
- ".py": "text/x-python",
25
- ".rb": "text/x-ruby",
26
- ".sh": "text/x-shellscript",
27
- ".bash": "text/x-shellscript",
28
- // Images
29
- ".png": "image/png",
30
- ".jpg": "image/jpeg",
31
- ".jpeg": "image/jpeg",
32
- ".gif": "image/gif",
33
- ".svg": "image/svg+xml",
34
- ".webp": "image/webp",
35
- ".ico": "image/x-icon",
36
- // Documents
37
- ".pdf": "application/pdf",
38
- // Archives
39
- ".zip": "application/zip",
40
- ".gz": "application/gzip",
41
- ".tar": "application/x-tar"
1
+ import { Storage } from "@google-cloud/storage";
2
+ import { FileExistsError, FileNotFoundError, MastraFilesystem } from "@mastra/core/workspace";
3
+ //#region src/filesystem/index.ts
4
+ /**
5
+ * GCS Filesystem Provider
6
+ *
7
+ * A filesystem implementation backed by Google Cloud Storage.
8
+ */
9
+ /**
10
+ * Common MIME types by file extension.
11
+ */
12
+ const MIME_TYPES = {
13
+ ".txt": "text/plain",
14
+ ".md": "text/markdown",
15
+ ".markdown": "text/markdown",
16
+ ".html": "text/html",
17
+ ".htm": "text/html",
18
+ ".css": "text/css",
19
+ ".csv": "text/csv",
20
+ ".xml": "text/xml",
21
+ ".js": "text/javascript",
22
+ ".mjs": "text/javascript",
23
+ ".ts": "text/typescript",
24
+ ".tsx": "text/typescript",
25
+ ".jsx": "text/javascript",
26
+ ".json": "application/json",
27
+ ".yaml": "text/yaml",
28
+ ".yml": "text/yaml",
29
+ ".py": "text/x-python",
30
+ ".rb": "text/x-ruby",
31
+ ".sh": "text/x-shellscript",
32
+ ".bash": "text/x-shellscript",
33
+ ".png": "image/png",
34
+ ".jpg": "image/jpeg",
35
+ ".jpeg": "image/jpeg",
36
+ ".gif": "image/gif",
37
+ ".svg": "image/svg+xml",
38
+ ".webp": "image/webp",
39
+ ".ico": "image/x-icon",
40
+ ".pdf": "application/pdf",
41
+ ".zip": "application/zip",
42
+ ".gz": "application/gzip",
43
+ ".tar": "application/x-tar"
42
44
  };
45
+ /**
46
+ * Get MIME type from file path extension.
47
+ */
43
48
  function getMimeType(path) {
44
- const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
45
- return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
49
+ const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
50
+ return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
46
51
  }
52
+ /**
53
+ * GCS filesystem implementation.
54
+ *
55
+ * Stores files in a Google Cloud Storage bucket.
56
+ * Supports mounting into E2B sandboxes via gcsfuse.
57
+ *
58
+ * @example Using Application Default Credentials
59
+ * ```typescript
60
+ * import { GCSFilesystem } from '@mastra/gcs';
61
+ *
62
+ * // Uses ADC (gcloud auth application-default login)
63
+ * const fs = new GCSFilesystem({
64
+ * bucket: 'my-bucket',
65
+ * projectId: 'my-project',
66
+ * });
67
+ * ```
68
+ *
69
+ * @example Using Service Account Key
70
+ * ```typescript
71
+ * import { GCSFilesystem } from '@mastra/gcs';
72
+ *
73
+ * const fs = new GCSFilesystem({
74
+ * bucket: 'my-bucket',
75
+ * projectId: 'my-project',
76
+ * credentials: {
77
+ * type: 'service_account',
78
+ * project_id: 'my-project',
79
+ * private_key_id: '...',
80
+ * private_key: '-----BEGIN PRIVATE KEY-----\n...',
81
+ * client_email: '...@...iam.gserviceaccount.com',
82
+ * // ... rest of service account key
83
+ * },
84
+ * });
85
+ * ```
86
+ *
87
+ * @example Using Key File Path
88
+ * ```typescript
89
+ * import { GCSFilesystem } from '@mastra/gcs';
90
+ *
91
+ * const fs = new GCSFilesystem({
92
+ * bucket: 'my-bucket',
93
+ * projectId: 'my-project',
94
+ * credentials: '/path/to/service-account-key.json',
95
+ * });
96
+ * ```
97
+ */
98
+ /** Trim leading and trailing slashes without regex (avoids polynomial regex on user input). */
47
99
  function trimSlashes(s) {
48
- let start = 0;
49
- let end = s.length;
50
- while (start < end && s[start] === "/") start++;
51
- while (end > start && s[end - 1] === "/") end--;
52
- return s.slice(start, end);
100
+ let start = 0;
101
+ let end = s.length;
102
+ while (start < end && s[start] === "/") start++;
103
+ while (end > start && s[end - 1] === "/") end--;
104
+ return s.slice(start, end);
53
105
  }
54
106
  var GCSFilesystem = class extends MastraFilesystem {
55
- id;
56
- name = "GCSFilesystem";
57
- provider = "gcs";
58
- readOnly;
59
- status = "pending";
60
- // Display metadata for UI
61
- displayName;
62
- icon = "gcs";
63
- description;
64
- bucketName;
65
- projectId;
66
- credentials;
67
- prefix;
68
- endpoint;
69
- _storage = null;
70
- _bucket = null;
71
- constructor(options) {
72
- super({ ...options, name: "GCSFilesystem" });
73
- this.id = options.id ?? `gcs-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
74
- this.bucketName = options.bucket;
75
- this.projectId = options.projectId;
76
- this.credentials = options.credentials;
77
- this.prefix = options.prefix ? trimSlashes(options.prefix) + "/" : "";
78
- this.endpoint = options.endpoint;
79
- this.displayName = options.displayName ?? "Google Cloud Storage";
80
- this.icon = options.icon ?? "gcs";
81
- this.description = options.description;
82
- this.readOnly = options.readOnly;
83
- }
84
- /**
85
- * Get the underlying Google Cloud Storage instance for direct access to GCS APIs.
86
- *
87
- * Use this when you need to access GCS features not exposed through the
88
- * WorkspaceFilesystem interface (e.g., signed URLs, IAM, custom metadata, etc.).
89
- *
90
- * @example Access other buckets
91
- * ```typescript
92
- * const storage = fs.storage;
93
- * const [buckets] = await storage.getBuckets();
94
- * ```
95
- */
96
- get storage() {
97
- return this.getStorage();
98
- }
99
- /**
100
- * Get the underlying GCS Bucket instance for direct access to bucket operations.
101
- *
102
- * Use this when you need to access bucket features not exposed through the
103
- * WorkspaceFilesystem interface (e.g., signed URLs, lifecycle rules, etc.).
104
- *
105
- * @example Generate a signed URL
106
- * ```typescript
107
- * const bucket = fs.bucket;
108
- * const [url] = await bucket.file('my-file.txt').getSignedUrl({
109
- * action: 'read',
110
- * expires: Date.now() + 15 * 60 * 1000,
111
- * });
112
- * ```
113
- */
114
- get bucket() {
115
- return this.getBucket();
116
- }
117
- /**
118
- * Get mount configuration for E2B sandbox.
119
- * Returns GCS-compatible config that works with gcsfuse.
120
- */
121
- getMountConfig() {
122
- const config = {
123
- type: "gcs",
124
- bucket: this.bucketName
125
- };
126
- if (this.credentials && typeof this.credentials === "object") {
127
- config.serviceAccountKey = JSON.stringify(this.credentials);
128
- }
129
- if (this.prefix) {
130
- config.prefix = this.prefix.replace(/\/$/, "");
131
- }
132
- return config;
133
- }
134
- /**
135
- * Get filesystem info for status reporting.
136
- */
137
- getInfo() {
138
- return {
139
- id: this.id,
140
- name: this.name,
141
- provider: this.provider,
142
- status: this.status,
143
- error: this.error,
144
- readOnly: this.readOnly,
145
- icon: this.icon,
146
- metadata: {
147
- bucket: this.bucketName,
148
- ...this.endpoint && { endpoint: this.endpoint },
149
- ...this.prefix && { prefix: this.prefix }
150
- }
151
- };
152
- }
153
- /**
154
- * Get instructions describing this GCS filesystem.
155
- * Used by agents to understand storage semantics.
156
- */
157
- getInstructions() {
158
- const access = this.readOnly ? "Read-only" : "Persistent";
159
- return `Google Cloud Storage in bucket "${this.bucketName}". ${access} storage - files are retained across sessions.`;
160
- }
161
- getStorage() {
162
- if (this._storage) return this._storage;
163
- const options = {};
164
- if (this.projectId) {
165
- options.projectId = this.projectId;
166
- }
167
- if (this.credentials) {
168
- if (typeof this.credentials === "string") {
169
- options.keyFilename = this.credentials;
170
- } else {
171
- options.credentials = this.credentials;
172
- }
173
- }
174
- if (this.endpoint) {
175
- options.apiEndpoint = this.endpoint;
176
- }
177
- this._storage = new Storage(options);
178
- return this._storage;
179
- }
180
- getBucket() {
181
- if (this._bucket) return this._bucket;
182
- const storage = this.getStorage();
183
- this._bucket = storage.bucket(this.bucketName);
184
- return this._bucket;
185
- }
186
- /**
187
- * Ensure the filesystem is initialized and return the bucket.
188
- * Uses base class ensureReady() for status management, then returns bucket.
189
- */
190
- async getReadyBucket() {
191
- await this.ensureReady();
192
- return this.getBucket();
193
- }
194
- toKey(path) {
195
- const cleanPath = path.replace(/^\/+/, "").replace(/^\.(?:\/|$)/, "");
196
- return this.prefix + cleanPath;
197
- }
198
- // ---------------------------------------------------------------------------
199
- // File Operations
200
- // ---------------------------------------------------------------------------
201
- async readFile(path, options) {
202
- const bucket = await this.getReadyBucket();
203
- const file = bucket.file(this.toKey(path));
204
- try {
205
- const [content] = await file.download();
206
- if (options?.encoding) {
207
- return content.toString(options.encoding);
208
- }
209
- return content;
210
- } catch (error) {
211
- if (error && typeof error === "object" && "code" in error && error.code === 404) {
212
- throw new FileNotFoundError(path);
213
- }
214
- throw error;
215
- }
216
- }
217
- async writeFile(path, content, options) {
218
- const bucket = await this.getReadyBucket();
219
- const file = bucket.file(this.toKey(path));
220
- if (options?.overwrite === false && await this.exists(path)) {
221
- throw new FileExistsError(path);
222
- }
223
- const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
224
- const contentType = getMimeType(path);
225
- await file.save(body, {
226
- contentType,
227
- resumable: false
228
- });
229
- }
230
- async appendFile(path, content) {
231
- let existing = "";
232
- try {
233
- existing = await this.readFile(path, { encoding: "utf-8" });
234
- } catch (error) {
235
- if (error instanceof FileNotFoundError) ; else {
236
- throw error;
237
- }
238
- }
239
- const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
240
- await this.writeFile(path, existing + appendContent);
241
- }
242
- async deleteFile(path, options) {
243
- const isDir = await this.isDirectory(path);
244
- if (isDir) {
245
- await this.rmdir(path, { recursive: true, force: options?.force });
246
- return;
247
- }
248
- const bucket = await this.getReadyBucket();
249
- const file = bucket.file(this.toKey(path));
250
- try {
251
- await file.delete();
252
- } catch (error) {
253
- if (!options?.force) {
254
- if (error && typeof error === "object" && "code" in error && error.code === 404) {
255
- throw new FileNotFoundError(path);
256
- }
257
- throw error;
258
- }
259
- }
260
- }
261
- async copyFile(src, dest, options) {
262
- const bucket = await this.getReadyBucket();
263
- const srcFile = bucket.file(this.toKey(src));
264
- const destFile = bucket.file(this.toKey(dest));
265
- if (options?.overwrite === false && await this.exists(dest)) {
266
- throw new FileExistsError(dest);
267
- }
268
- try {
269
- await srcFile.copy(destFile);
270
- } catch (error) {
271
- if (error && typeof error === "object" && "code" in error && error.code === 404) {
272
- throw new FileNotFoundError(src);
273
- }
274
- throw error;
275
- }
276
- }
277
- async moveFile(src, dest, options) {
278
- await this.copyFile(src, dest, options);
279
- await this.deleteFile(src, { force: true });
280
- }
281
- // ---------------------------------------------------------------------------
282
- // Directory Operations
283
- // ---------------------------------------------------------------------------
284
- async mkdir(_path, _options) {
285
- }
286
- async rmdir(path, options) {
287
- if (!options?.recursive) {
288
- const bucket2 = await this.getReadyBucket();
289
- const prefix2 = this.toKey(path).replace(/\/$/, "") + "/";
290
- const [files] = await bucket2.getFiles({ prefix: prefix2, maxResults: 1 });
291
- if (files.length > 0) {
292
- throw new Error(`Directory not empty: ${path}`);
293
- }
294
- return;
295
- }
296
- const bucket = await this.getReadyBucket();
297
- const prefix = this.toKey(path).replace(/\/$/, "") + "/";
298
- await bucket.deleteFiles({ prefix });
299
- }
300
- async readdir(path, options) {
301
- const bucket = await this.getReadyBucket();
302
- const prefix = this.toKey(path).replace(/\/$/, "");
303
- const searchPrefix = prefix ? prefix + "/" : "";
304
- const entries = [];
305
- const seenDirs = /* @__PURE__ */ new Set();
306
- const [files] = await bucket.getFiles({
307
- prefix: searchPrefix,
308
- autoPaginate: true
309
- });
310
- for (const file of files) {
311
- const key = file.name;
312
- if (!key || key === searchPrefix) continue;
313
- const relativePath = key.slice(searchPrefix.length);
314
- if (!relativePath) continue;
315
- if (relativePath.endsWith("/")) {
316
- const dirName = relativePath.slice(0, -1);
317
- if (!seenDirs.has(dirName)) {
318
- seenDirs.add(dirName);
319
- entries.push({ name: dirName, type: "directory" });
320
- }
321
- continue;
322
- }
323
- const name = options?.recursive ? relativePath : relativePath.split("/")[0];
324
- if (!name) continue;
325
- if (!options?.recursive && relativePath.includes("/")) {
326
- if (!seenDirs.has(name)) {
327
- seenDirs.add(name);
328
- entries.push({ name, type: "directory" });
329
- }
330
- continue;
331
- }
332
- if (options?.extension) {
333
- const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];
334
- if (!extensions.some((ext) => name.endsWith(ext))) {
335
- continue;
336
- }
337
- }
338
- entries.push({
339
- name,
340
- type: "file",
341
- size: file.metadata.size != null ? Number(file.metadata.size) : void 0
342
- });
343
- }
344
- return entries;
345
- }
346
- // ---------------------------------------------------------------------------
347
- // Path Operations
348
- // ---------------------------------------------------------------------------
349
- async exists(path) {
350
- const key = this.toKey(path);
351
- if (!key) return true;
352
- const bucket = await this.getReadyBucket();
353
- const file = bucket.file(key);
354
- const [exists] = await file.exists();
355
- if (exists) return true;
356
- const [files] = await bucket.getFiles({
357
- prefix: key.replace(/\/$/, "") + "/",
358
- maxResults: 1
359
- });
360
- return files.length > 0;
361
- }
362
- async stat(path) {
363
- const key = this.toKey(path);
364
- if (!key) {
365
- return {
366
- name: "",
367
- path,
368
- type: "directory",
369
- size: 0,
370
- createdAt: /* @__PURE__ */ new Date(),
371
- modifiedAt: /* @__PURE__ */ new Date()
372
- };
373
- }
374
- const bucket = await this.getReadyBucket();
375
- const file = bucket.file(key);
376
- const [exists] = await file.exists();
377
- if (exists) {
378
- const [metadata] = await file.getMetadata();
379
- const name = path.split("/").pop() ?? "";
380
- return {
381
- name,
382
- path,
383
- type: "file",
384
- size: Number(metadata.size) || 0,
385
- // read_file tool gates the native media-part path on `stat.mimeType`.
386
- mimeType: typeof metadata.contentType === "string" ? metadata.contentType : getMimeType(path),
387
- createdAt: metadata.timeCreated ? new Date(metadata.timeCreated) : /* @__PURE__ */ new Date(),
388
- modifiedAt: metadata.updated ? new Date(metadata.updated) : /* @__PURE__ */ new Date()
389
- };
390
- }
391
- const isDir = await this.isDirectory(path);
392
- if (isDir) {
393
- const name = path.split("/").filter(Boolean).pop() ?? "";
394
- return {
395
- name,
396
- path,
397
- type: "directory",
398
- size: 0,
399
- createdAt: /* @__PURE__ */ new Date(),
400
- modifiedAt: /* @__PURE__ */ new Date()
401
- };
402
- }
403
- throw new FileNotFoundError(path);
404
- }
405
- async isFile(path) {
406
- const key = this.toKey(path);
407
- if (!key) return false;
408
- const bucket = await this.getReadyBucket();
409
- const file = bucket.file(key);
410
- const [exists] = await file.exists();
411
- return exists;
412
- }
413
- async isDirectory(path) {
414
- const key = this.toKey(path);
415
- if (!key) return true;
416
- const bucket = await this.getReadyBucket();
417
- const [files] = await bucket.getFiles({
418
- prefix: key.replace(/\/$/, "") + "/",
419
- maxResults: 1
420
- });
421
- return files.length > 0;
422
- }
423
- // ---------------------------------------------------------------------------
424
- // Lifecycle (overrides base class protected methods)
425
- // ---------------------------------------------------------------------------
426
- /**
427
- * Initialize the GCS client.
428
- * Status management is handled by the base class.
429
- */
430
- async init() {
431
- const bucket = this.getBucket();
432
- try {
433
- const [exists] = await bucket.exists();
434
- if (!exists) {
435
- const err = new Error(`Bucket "${this.bucketName}" does not exist`);
436
- err.status = 404;
437
- throw err;
438
- }
439
- } catch (error) {
440
- if (error.status) {
441
- throw error;
442
- }
443
- const code = error.code;
444
- if (typeof code === "number") {
445
- const message = error instanceof Error ? error.message : String(error);
446
- const err = new Error(
447
- message
448
- // code === 403
449
- // ? `Access denied to bucket "${this.bucketName}" - check credentials and permissions`
450
- // : message,
451
- );
452
- err.status = code;
453
- throw err;
454
- }
455
- throw error;
456
- }
457
- }
458
- /**
459
- * Clean up the GCS client.
460
- * Status management is handled by the base class.
461
- */
462
- async destroy() {
463
- this._storage = null;
464
- this._bucket = null;
465
- }
107
+ id;
108
+ name = "GCSFilesystem";
109
+ provider = "gcs";
110
+ readOnly;
111
+ status = "pending";
112
+ displayName;
113
+ icon = "gcs";
114
+ description;
115
+ bucketName;
116
+ projectId;
117
+ credentials;
118
+ prefix;
119
+ endpoint;
120
+ _storage = null;
121
+ _bucket = null;
122
+ constructor(options) {
123
+ super({
124
+ ...options,
125
+ name: "GCSFilesystem"
126
+ });
127
+ this.id = options.id ?? `gcs-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
128
+ this.bucketName = options.bucket;
129
+ this.projectId = options.projectId;
130
+ this.credentials = options.credentials;
131
+ this.prefix = options.prefix ? trimSlashes(options.prefix) + "/" : "";
132
+ this.endpoint = options.endpoint;
133
+ this.displayName = options.displayName ?? "Google Cloud Storage";
134
+ this.icon = options.icon ?? "gcs";
135
+ this.description = options.description;
136
+ this.readOnly = options.readOnly;
137
+ }
138
+ /**
139
+ * Get the underlying Google Cloud Storage instance for direct access to GCS APIs.
140
+ *
141
+ * Use this when you need to access GCS features not exposed through the
142
+ * WorkspaceFilesystem interface (e.g., signed URLs, IAM, custom metadata, etc.).
143
+ *
144
+ * @example Access other buckets
145
+ * ```typescript
146
+ * const storage = fs.storage;
147
+ * const [buckets] = await storage.getBuckets();
148
+ * ```
149
+ */
150
+ get storage() {
151
+ return this.getStorage();
152
+ }
153
+ /**
154
+ * Get the underlying GCS Bucket instance for direct access to bucket operations.
155
+ *
156
+ * Use this when you need to access bucket features not exposed through the
157
+ * WorkspaceFilesystem interface (e.g., signed URLs, lifecycle rules, etc.).
158
+ *
159
+ * @example Generate a signed URL
160
+ * ```typescript
161
+ * const bucket = fs.bucket;
162
+ * const [url] = await bucket.file('my-file.txt').getSignedUrl({
163
+ * action: 'read',
164
+ * expires: Date.now() + 15 * 60 * 1000,
165
+ * });
166
+ * ```
167
+ */
168
+ get bucket() {
169
+ return this.getBucket();
170
+ }
171
+ /**
172
+ * Get mount configuration for E2B sandbox.
173
+ * Returns GCS-compatible config that works with gcsfuse.
174
+ */
175
+ getMountConfig() {
176
+ const config = {
177
+ type: "gcs",
178
+ bucket: this.bucketName
179
+ };
180
+ if (this.credentials && typeof this.credentials === "object") config.serviceAccountKey = JSON.stringify(this.credentials);
181
+ if (this.prefix) config.prefix = this.prefix.replace(/\/$/, "");
182
+ return config;
183
+ }
184
+ /**
185
+ * Get filesystem info for status reporting.
186
+ */
187
+ getInfo() {
188
+ return {
189
+ id: this.id,
190
+ name: this.name,
191
+ provider: this.provider,
192
+ status: this.status,
193
+ error: this.error,
194
+ readOnly: this.readOnly,
195
+ icon: this.icon,
196
+ metadata: {
197
+ bucket: this.bucketName,
198
+ ...this.endpoint && { endpoint: this.endpoint },
199
+ ...this.prefix && { prefix: this.prefix }
200
+ }
201
+ };
202
+ }
203
+ /**
204
+ * Get instructions describing this GCS filesystem.
205
+ * Used by agents to understand storage semantics.
206
+ */
207
+ getInstructions() {
208
+ const access = this.readOnly ? "Read-only" : "Persistent";
209
+ return `Google Cloud Storage in bucket "${this.bucketName}". ${access} storage - files are retained across sessions.`;
210
+ }
211
+ getStorage() {
212
+ if (this._storage) return this._storage;
213
+ const options = {};
214
+ if (this.projectId) options.projectId = this.projectId;
215
+ if (this.credentials) if (typeof this.credentials === "string") options.keyFilename = this.credentials;
216
+ else options.credentials = this.credentials;
217
+ if (this.endpoint) options.apiEndpoint = this.endpoint;
218
+ this._storage = new Storage(options);
219
+ return this._storage;
220
+ }
221
+ getBucket() {
222
+ if (this._bucket) return this._bucket;
223
+ const storage = this.getStorage();
224
+ this._bucket = storage.bucket(this.bucketName);
225
+ return this._bucket;
226
+ }
227
+ /**
228
+ * Ensure the filesystem is initialized and return the bucket.
229
+ * Uses base class ensureReady() for status management, then returns bucket.
230
+ */
231
+ async getReadyBucket() {
232
+ await this.ensureReady();
233
+ return this.getBucket();
234
+ }
235
+ toKey(path) {
236
+ const cleanPath = path.replace(/^\/+/, "").replace(/^\.(?:\/|$)/, "");
237
+ return this.prefix + cleanPath;
238
+ }
239
+ async readFile(path, options) {
240
+ if (path.endsWith("/")) throw new FileNotFoundError(path);
241
+ const file = (await this.getReadyBucket()).file(this.toKey(path));
242
+ try {
243
+ const [content] = await file.download();
244
+ if (options?.encoding) return content.toString(options.encoding);
245
+ return content;
246
+ } catch (error) {
247
+ if (error && typeof error === "object" && "code" in error && error.code === 404) throw new FileNotFoundError(path);
248
+ throw error;
249
+ }
250
+ }
251
+ async writeFile(path, content, options) {
252
+ const file = (await this.getReadyBucket()).file(this.toKey(path));
253
+ if (options?.overwrite === false && await this.exists(path)) throw new FileExistsError(path);
254
+ const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
255
+ const contentType = getMimeType(path);
256
+ await file.save(body, {
257
+ contentType,
258
+ resumable: false
259
+ });
260
+ }
261
+ async appendFile(path, content) {
262
+ let existing = "";
263
+ try {
264
+ existing = await this.readFile(path, { encoding: "utf-8" });
265
+ } catch (error) {
266
+ if (error instanceof FileNotFoundError) {} else throw error;
267
+ }
268
+ const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
269
+ await this.writeFile(path, existing + appendContent);
270
+ }
271
+ async deleteFile(path, options) {
272
+ if (await this.isDirectory(path)) {
273
+ await this.rmdir(path, {
274
+ recursive: true,
275
+ force: options?.force
276
+ });
277
+ return;
278
+ }
279
+ const file = (await this.getReadyBucket()).file(this.toKey(path));
280
+ try {
281
+ await file.delete();
282
+ } catch (error) {
283
+ if (!options?.force) {
284
+ if (error && typeof error === "object" && "code" in error && error.code === 404) throw new FileNotFoundError(path);
285
+ throw error;
286
+ }
287
+ }
288
+ }
289
+ async copyFile(src, dest, options) {
290
+ if (src.endsWith("/")) throw new FileNotFoundError(src);
291
+ const bucket = await this.getReadyBucket();
292
+ const srcFile = bucket.file(this.toKey(src));
293
+ const destFile = bucket.file(this.toKey(dest));
294
+ if (options?.overwrite === false && await this.exists(dest)) throw new FileExistsError(dest);
295
+ try {
296
+ await srcFile.copy(destFile);
297
+ } catch (error) {
298
+ if (error && typeof error === "object" && "code" in error && error.code === 404) throw new FileNotFoundError(src);
299
+ throw error;
300
+ }
301
+ }
302
+ async moveFile(src, dest, options) {
303
+ await this.copyFile(src, dest, options);
304
+ await this.deleteFile(src, { force: true });
305
+ }
306
+ async mkdir(path, _options) {
307
+ const key = trimSlashes(this.toKey(path));
308
+ if (!key || key + "/" === this.prefix) return;
309
+ const bucket = await this.getReadyBucket();
310
+ let fileExists = false;
311
+ try {
312
+ [fileExists] = await bucket.file(key).exists();
313
+ } catch {
314
+ fileExists = false;
315
+ }
316
+ if (fileExists) throw new FileExistsError(path);
317
+ await bucket.file(key + "/").save(Buffer.alloc(0), { resumable: false });
318
+ }
319
+ async rmdir(path, options) {
320
+ if (!options?.recursive) {
321
+ const bucket = await this.getReadyBucket();
322
+ const key = trimSlashes(this.toKey(path));
323
+ const prefix = key + "/";
324
+ const [files] = await bucket.getFiles({
325
+ prefix,
326
+ maxResults: 2
327
+ });
328
+ if (files.some((file) => file.name !== prefix)) throw new Error(`Directory not empty: ${path}`);
329
+ if (key && prefix !== this.prefix) await bucket.file(prefix).delete({ ignoreNotFound: true });
330
+ return;
331
+ }
332
+ const bucket = await this.getReadyBucket();
333
+ const prefix = this.toKey(path).replace(/\/$/, "") + "/";
334
+ await bucket.deleteFiles({ prefix });
335
+ }
336
+ async readdir(path, options) {
337
+ const bucket = await this.getReadyBucket();
338
+ const prefix = this.toKey(path).replace(/\/$/, "");
339
+ const searchPrefix = prefix ? prefix + "/" : "";
340
+ const entries = [];
341
+ const seenDirs = /* @__PURE__ */ new Set();
342
+ const [files] = await bucket.getFiles({
343
+ prefix: searchPrefix,
344
+ autoPaginate: true
345
+ });
346
+ for (const file of files) {
347
+ const key = file.name;
348
+ if (!key || key === searchPrefix) continue;
349
+ const relativePath = key.slice(searchPrefix.length);
350
+ if (!relativePath) continue;
351
+ if (relativePath.endsWith("/")) {
352
+ if (options?.recursive && options.extension) continue;
353
+ const dirName = options?.recursive ? relativePath.slice(0, -1) : relativePath.split("/")[0];
354
+ if (!dirName) continue;
355
+ if (!seenDirs.has(dirName)) {
356
+ seenDirs.add(dirName);
357
+ entries.push({
358
+ name: dirName,
359
+ type: "directory"
360
+ });
361
+ }
362
+ continue;
363
+ }
364
+ const name = options?.recursive ? relativePath : relativePath.split("/")[0];
365
+ if (!name) continue;
366
+ if (!options?.recursive && relativePath.includes("/")) {
367
+ if (!seenDirs.has(name)) {
368
+ seenDirs.add(name);
369
+ entries.push({
370
+ name,
371
+ type: "directory"
372
+ });
373
+ }
374
+ continue;
375
+ }
376
+ if (options?.extension) {
377
+ if (!(Array.isArray(options.extension) ? options.extension : [options.extension]).some((ext) => name.endsWith(ext))) continue;
378
+ }
379
+ entries.push({
380
+ name,
381
+ type: "file",
382
+ size: file.metadata.size != null ? Number(file.metadata.size) : void 0
383
+ });
384
+ }
385
+ return entries;
386
+ }
387
+ async exists(path) {
388
+ const key = this.toKey(path);
389
+ if (!key) return true;
390
+ const bucket = await this.getReadyBucket();
391
+ const [exists] = await bucket.file(key).exists();
392
+ if (exists) return true;
393
+ const [files] = await bucket.getFiles({
394
+ prefix: key.replace(/\/$/, "") + "/",
395
+ maxResults: 1
396
+ });
397
+ return files.length > 0;
398
+ }
399
+ async stat(path) {
400
+ const key = trimSlashes(this.toKey(path));
401
+ const directoryOnly = path.endsWith("/");
402
+ if (!key) return {
403
+ name: "",
404
+ path,
405
+ type: "directory",
406
+ size: 0,
407
+ createdAt: /* @__PURE__ */ new Date(),
408
+ modifiedAt: /* @__PURE__ */ new Date()
409
+ };
410
+ const file = (await this.getReadyBucket()).file(key);
411
+ const name = key.split("/").pop() ?? "";
412
+ const [exists] = directoryOnly ? [false] : await file.exists();
413
+ if (exists) {
414
+ const [metadata] = await file.getMetadata();
415
+ return {
416
+ name,
417
+ path,
418
+ type: "file",
419
+ size: Number(metadata.size) || 0,
420
+ mimeType: typeof metadata.contentType === "string" ? metadata.contentType : getMimeType(path),
421
+ createdAt: metadata.timeCreated ? new Date(metadata.timeCreated) : /* @__PURE__ */ new Date(),
422
+ modifiedAt: metadata.updated ? new Date(metadata.updated) : /* @__PURE__ */ new Date()
423
+ };
424
+ }
425
+ if (await this.isDirectory(path)) return {
426
+ name,
427
+ path,
428
+ type: "directory",
429
+ size: 0,
430
+ createdAt: /* @__PURE__ */ new Date(),
431
+ modifiedAt: /* @__PURE__ */ new Date()
432
+ };
433
+ throw new FileNotFoundError(path);
434
+ }
435
+ async isFile(path) {
436
+ if (path.endsWith("/")) return false;
437
+ const key = trimSlashes(this.toKey(path));
438
+ if (!key) return false;
439
+ const [exists] = await (await this.getReadyBucket()).file(key).exists();
440
+ return exists;
441
+ }
442
+ async isDirectory(path) {
443
+ const key = this.toKey(path);
444
+ if (!key) return true;
445
+ const [files] = await (await this.getReadyBucket()).getFiles({
446
+ prefix: key.replace(/\/$/, "") + "/",
447
+ maxResults: 1
448
+ });
449
+ return files.length > 0;
450
+ }
451
+ /**
452
+ * Initialize the GCS client.
453
+ * Status management is handled by the base class.
454
+ */
455
+ async init() {
456
+ const bucket = this.getBucket();
457
+ try {
458
+ const [exists] = await bucket.exists();
459
+ if (!exists) {
460
+ const err = /* @__PURE__ */ new Error(`Bucket "${this.bucketName}" does not exist`);
461
+ err.status = 404;
462
+ throw err;
463
+ }
464
+ } catch (error) {
465
+ if (error.status) throw error;
466
+ const code = error.code;
467
+ if (typeof code === "number") {
468
+ const message = error instanceof Error ? error.message : String(error);
469
+ const err = new Error(message);
470
+ err.status = code;
471
+ throw err;
472
+ }
473
+ throw error;
474
+ }
475
+ }
476
+ /**
477
+ * Clean up the GCS client.
478
+ * Status management is handled by the base class.
479
+ */
480
+ async destroy() {
481
+ this._storage = null;
482
+ this._bucket = null;
483
+ }
466
484
  };
467
-
468
- // src/provider.ts
469
- var gcsFilesystemProvider = {
470
- id: "gcs",
471
- name: "Google Cloud Storage",
472
- description: "Google Cloud Storage bucket",
473
- configSchema: {
474
- type: "object",
475
- required: ["bucket"],
476
- properties: {
477
- bucket: { type: "string", description: "GCS bucket name" },
478
- projectId: { type: "string", description: "GCS project ID" },
479
- credentials: {
480
- description: "Service account key JSON object or path to key file",
481
- oneOf: [{ type: "object" }, { type: "string" }]
482
- },
483
- prefix: { type: "string", description: "Key prefix (acts like a subdirectory)" },
484
- readOnly: { type: "boolean", description: "Mount as read-only", default: false },
485
- endpoint: { type: "string", description: "Custom API endpoint URL (for local emulators)" }
486
- }
487
- },
488
- createFilesystem: (config) => new GCSFilesystem(config)
485
+ //#endregion
486
+ //#region src/provider.ts
487
+ const gcsFilesystemProvider = {
488
+ id: "gcs",
489
+ name: "Google Cloud Storage",
490
+ description: "Google Cloud Storage bucket",
491
+ configSchema: {
492
+ type: "object",
493
+ required: ["bucket"],
494
+ properties: {
495
+ bucket: {
496
+ type: "string",
497
+ description: "GCS bucket name"
498
+ },
499
+ projectId: {
500
+ type: "string",
501
+ description: "GCS project ID"
502
+ },
503
+ credentials: {
504
+ description: "Service account key JSON object or path to key file",
505
+ oneOf: [{ type: "object" }, { type: "string" }]
506
+ },
507
+ prefix: {
508
+ type: "string",
509
+ description: "Key prefix (acts like a subdirectory)"
510
+ },
511
+ readOnly: {
512
+ type: "boolean",
513
+ description: "Mount as read-only",
514
+ default: false
515
+ },
516
+ endpoint: {
517
+ type: "string",
518
+ description: "Custom API endpoint URL (for local emulators)"
519
+ }
520
+ }
521
+ },
522
+ createFilesystem: (config) => new GCSFilesystem(config)
489
523
  };
490
-
524
+ //#endregion
491
525
  export { GCSFilesystem, gcsFilesystemProvider };
492
- //# sourceMappingURL=index.js.map
526
+
493
527
  //# sourceMappingURL=index.js.map