@aigne/afs-gcs 1.11.0-beta.6

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.mjs ADDED
@@ -0,0 +1,1055 @@
1
+ import { AFSBaseProvider, AFSError, AFSNotFoundError, Actions, Delete, List, Meta, Read, Stat, Write } from "@aigne/afs";
2
+ import { camelize, optionalize, zodParse } from "@aigne/afs/utils/zod";
3
+ import { Storage } from "@google-cloud/storage";
4
+ import { z } from "zod";
5
+
6
+ //#region src/cache.ts
7
+ /**
8
+ * LRU Cache with TTL support
9
+ */
10
+ var LRUCache = class {
11
+ cache = /* @__PURE__ */ new Map();
12
+ maxSize;
13
+ defaultTtl;
14
+ /**
15
+ * Create a new LRU cache
16
+ *
17
+ * @param maxSize - Maximum number of entries (default: 1000)
18
+ * @param defaultTtl - Default TTL in seconds (default: 60)
19
+ */
20
+ constructor(maxSize = 1e3, defaultTtl = 60) {
21
+ this.maxSize = maxSize;
22
+ this.defaultTtl = defaultTtl;
23
+ }
24
+ /**
25
+ * Get a value from the cache
26
+ *
27
+ * @param key - Cache key
28
+ * @returns Cached value or undefined if not found/expired
29
+ */
30
+ get(key) {
31
+ const entry = this.cache.get(key);
32
+ if (!entry) return;
33
+ if (Date.now() > entry.expiresAt) {
34
+ this.cache.delete(key);
35
+ return;
36
+ }
37
+ this.cache.delete(key);
38
+ this.cache.set(key, entry);
39
+ return entry.value;
40
+ }
41
+ /**
42
+ * Set a value in the cache
43
+ *
44
+ * @param key - Cache key
45
+ * @param value - Value to cache
46
+ * @param ttl - TTL in seconds (optional, uses default)
47
+ */
48
+ set(key, value, ttl) {
49
+ if (this.cache.has(key)) this.cache.delete(key);
50
+ while (this.cache.size >= this.maxSize) {
51
+ const oldestKey = this.cache.keys().next().value;
52
+ if (oldestKey) this.cache.delete(oldestKey);
53
+ }
54
+ const expiresAt = Date.now() + (ttl ?? this.defaultTtl) * 1e3;
55
+ this.cache.set(key, {
56
+ value,
57
+ expiresAt
58
+ });
59
+ }
60
+ /**
61
+ * Delete a value from the cache
62
+ *
63
+ * @param key - Cache key
64
+ */
65
+ delete(key) {
66
+ this.cache.delete(key);
67
+ }
68
+ /**
69
+ * Delete all entries matching a prefix
70
+ *
71
+ * @param prefix - Key prefix to match
72
+ */
73
+ deleteByPrefix(prefix) {
74
+ for (const key of this.cache.keys()) if (key.startsWith(prefix)) this.cache.delete(key);
75
+ }
76
+ /**
77
+ * Clear all entries
78
+ */
79
+ clear() {
80
+ this.cache.clear();
81
+ }
82
+ /**
83
+ * Get the number of entries in the cache
84
+ */
85
+ get size() {
86
+ return this.cache.size;
87
+ }
88
+ /**
89
+ * Prune expired entries
90
+ */
91
+ prune() {
92
+ const now = Date.now();
93
+ for (const [key, entry] of this.cache.entries()) if (now > entry.expiresAt) this.cache.delete(key);
94
+ }
95
+ };
96
+ /**
97
+ * Create a cache key from bucket, prefix, and path
98
+ */
99
+ function createCacheKey(bucket, prefix, path, suffix) {
100
+ const base = `${bucket}:${prefix}:${path}`;
101
+ return suffix ? `${base}:${suffix}` : base;
102
+ }
103
+
104
+ //#endregion
105
+ //#region src/client.ts
106
+ /**
107
+ * GCS Client Factory
108
+ *
109
+ * Creates configured Google Cloud Storage clients.
110
+ */
111
+ /**
112
+ * Create a GCS Storage client from options
113
+ *
114
+ * @param options - AFSGCS options
115
+ * @returns Configured Storage client
116
+ */
117
+ function createGCSClient(options) {
118
+ const storageOptions = {};
119
+ if (options.projectId) storageOptions.projectId = options.projectId;
120
+ if (options.endpoint) storageOptions.apiEndpoint = options.endpoint;
121
+ if (options.keyFilename) storageOptions.keyFilename = options.keyFilename;
122
+ if (options.credentials) storageOptions.credentials = {
123
+ client_email: options.credentials.clientEmail,
124
+ private_key: options.credentials.privateKey
125
+ };
126
+ return new Storage(storageOptions);
127
+ }
128
+
129
+ //#endregion
130
+ //#region src/errors.ts
131
+ /**
132
+ * GCS Provider Error Handling
133
+ *
134
+ * Maps GCS SDK errors to AFS error types.
135
+ */
136
+ /**
137
+ * AFS error codes
138
+ */
139
+ const AFSErrorCode = {
140
+ ENTRY_NOT_FOUND: "ENTRY_NOT_FOUND",
141
+ MODULE_NOT_FOUND: "MODULE_NOT_FOUND",
142
+ PERMISSION_DENIED: "PERMISSION_DENIED",
143
+ AUTH_ERROR: "AUTH_ERROR",
144
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
145
+ INVALID_OPERATION: "INVALID_OPERATION",
146
+ ALREADY_EXISTS: "ALREADY_EXISTS",
147
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
148
+ UNKNOWN: "UNKNOWN"
149
+ };
150
+ /**
151
+ * AFS Error class
152
+ */
153
+ var AFSError$1 = class extends Error {
154
+ code;
155
+ retryAfter;
156
+ constructor(code, message, options) {
157
+ super(message);
158
+ this.name = "AFSError";
159
+ this.code = code;
160
+ this.retryAfter = options?.retryAfter;
161
+ }
162
+ };
163
+ /**
164
+ * Custom GCS error class for internal use
165
+ */
166
+ var GCSError = class extends Error {
167
+ constructor(message, code) {
168
+ super(message);
169
+ this.code = code;
170
+ this.name = "GCSError";
171
+ }
172
+ };
173
+ /**
174
+ * Map GCS HTTP status codes to AFS error codes
175
+ */
176
+ const STATUS_TO_AFS_ERROR = {
177
+ 400: AFSErrorCode.INVALID_OPERATION,
178
+ 401: AFSErrorCode.AUTH_ERROR,
179
+ 403: AFSErrorCode.PERMISSION_DENIED,
180
+ 404: AFSErrorCode.ENTRY_NOT_FOUND,
181
+ 409: AFSErrorCode.ALREADY_EXISTS,
182
+ 429: AFSErrorCode.RATE_LIMIT_EXCEEDED,
183
+ 503: AFSErrorCode.SERVICE_UNAVAILABLE
184
+ };
185
+ /**
186
+ * Map GCS error to AFS error
187
+ *
188
+ * @param error - GCS SDK error or generic error
189
+ * @returns AFSError with appropriate error code
190
+ */
191
+ function mapGCSError(error) {
192
+ if (error instanceof GCSError) return new AFSError$1(STATUS_TO_AFS_ERROR[error.code] ?? AFSErrorCode.UNKNOWN, error.message);
193
+ if (typeof error === "object" && error !== null && "code" in error) {
194
+ const err = error;
195
+ const statusCode = typeof err.code === "number" ? err.code : 500;
196
+ const message = err.message ?? "Unknown GCS error";
197
+ return new AFSError$1(STATUS_TO_AFS_ERROR[statusCode] ?? AFSErrorCode.UNKNOWN, message);
198
+ }
199
+ if (error instanceof Error) return new AFSError$1(AFSErrorCode.UNKNOWN, error.message);
200
+ return new AFSError$1(AFSErrorCode.UNKNOWN, String(error));
201
+ }
202
+
203
+ //#endregion
204
+ //#region src/platform-ref.ts
205
+ /**
206
+ * Generate platform reference with GCP Console URL
207
+ *
208
+ * @param bucket - GCS bucket name
209
+ * @param prefix - Object prefix/key (without leading slash)
210
+ * @param isDirectory - Whether this is a directory (prefix)
211
+ * @returns Platform reference with console URL
212
+ */
213
+ function generatePlatformRef(bucket, prefix, isDirectory) {
214
+ if (isDirectory) return { consoleUrl: `https://console.cloud.google.com/storage/browser/${bucket}/${prefix.endsWith("/") ? prefix : prefix ? `${prefix}/` : ""}` };
215
+ return { consoleUrl: `https://console.cloud.google.com/storage/browser/_details/${bucket}/${encodeURIComponent(prefix).replace(/%2F/g, "/")}` };
216
+ }
217
+
218
+ //#endregion
219
+ //#region src/types.ts
220
+ /**
221
+ * AFS GCS Provider Types
222
+ */
223
+ /**
224
+ * GCS bucket name validation regex
225
+ * - 3-63 characters
226
+ * - lowercase letters, numbers, hyphens, dots (but not consecutive dots)
227
+ * - must start and end with letter or number
228
+ */
229
+ const BUCKET_NAME_REGEX = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
230
+ /**
231
+ * Additional validation for bucket names
232
+ */
233
+ function isValidBucketName(name) {
234
+ if (!BUCKET_NAME_REGEX.test(name)) return false;
235
+ if (name.includes("..")) return false;
236
+ if (name.includes("_")) return false;
237
+ return true;
238
+ }
239
+ /**
240
+ * Zod schema for options validation
241
+ */
242
+ const afsgcsOptionsSchema = camelize(z.object({
243
+ name: optionalize(z.string()),
244
+ description: optionalize(z.string()),
245
+ bucket: z.string().refine(isValidBucketName, "Invalid GCS bucket name"),
246
+ prefix: optionalize(z.string()),
247
+ projectId: optionalize(z.string()),
248
+ accessMode: optionalize(z.enum(["readonly", "readwrite"])),
249
+ endpoint: optionalize(z.string().url()),
250
+ keyFilename: optionalize(z.string()),
251
+ credentials: optionalize(z.object({
252
+ clientEmail: z.string(),
253
+ privateKey: z.string()
254
+ })),
255
+ cacheTtl: optionalize(z.number().int().min(0))
256
+ }).strict());
257
+
258
+ //#endregion
259
+ //#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
260
+ function __decorate(decorators, target, key, desc) {
261
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
262
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
263
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
264
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
265
+ }
266
+
267
+ //#endregion
268
+ //#region src/gcs-afs.ts
269
+ /**
270
+ * AFS GCS Provider
271
+ *
272
+ * GCS provider using AFSBaseProvider decorator routing pattern.
273
+ * Provides access to Google Cloud Storage through AFS.
274
+ */
275
+ /**
276
+ * Default URL expiration time (1 hour)
277
+ */
278
+ const DEFAULT_EXPIRES_IN = 3600;
279
+ /**
280
+ * Maximum expiration time (7 days)
281
+ */
282
+ const MAX_EXPIRES_IN = 604800;
283
+ /**
284
+ * Maximum sources for compose operation
285
+ */
286
+ const MAX_COMPOSE_SOURCES = 32;
287
+ /**
288
+ * AFSGCS Provider using Base Provider pattern
289
+ *
290
+ * Provides access to Google Cloud Storage through AFS.
291
+ * Uses decorator routing (@List, @Read, @Write, @Delete, @Meta, @Actions).
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const gcs = new AFSGCS({
296
+ * bucket: "my-bucket",
297
+ * prefix: "data",
298
+ * projectId: "my-project",
299
+ * });
300
+ *
301
+ * // Mount to AFS
302
+ * afs.mount(gcs);
303
+ *
304
+ * // List objects
305
+ * const result = await afs.list("/modules/my-bucket/data");
306
+ *
307
+ * // Read object
308
+ * const content = await afs.read("/modules/my-bucket/data/file.json");
309
+ * ```
310
+ */
311
+ var AFSGCS = class AFSGCS extends AFSBaseProvider {
312
+ name;
313
+ description;
314
+ accessMode;
315
+ options;
316
+ storage;
317
+ bucket;
318
+ listCache;
319
+ statCache;
320
+ constructor(options) {
321
+ super();
322
+ const parsed = afsgcsOptionsSchema.parse(options);
323
+ this.options = {
324
+ ...parsed,
325
+ bucket: parsed.bucket,
326
+ prefix: parsed.prefix ?? "",
327
+ accessMode: parsed.accessMode ?? "readonly"
328
+ };
329
+ this.name = parsed.name ?? parsed.bucket;
330
+ this.description = parsed.description ?? `GCS bucket: ${parsed.bucket}`;
331
+ this.accessMode = this.options.accessMode ?? "readonly";
332
+ this.storage = createGCSClient(this.options);
333
+ this.bucket = this.storage.bucket(parsed.bucket);
334
+ if (parsed.cacheTtl && parsed.cacheTtl > 0) {
335
+ this.listCache = new LRUCache(1e3, parsed.cacheTtl);
336
+ this.statCache = new LRUCache(5e3, parsed.cacheTtl);
337
+ }
338
+ }
339
+ /**
340
+ * Schema for configuration validation
341
+ */
342
+ static schema() {
343
+ return afsgcsOptionsSchema;
344
+ }
345
+ /**
346
+ * Load from configuration file
347
+ */
348
+ static async load(params) {
349
+ return new AFSGCS(zodParse(afsgcsOptionsSchema, params.parsed, { prefix: params.filepath }));
350
+ }
351
+ /**
352
+ * Build the full GCS key from a path
353
+ */
354
+ buildGCSKey(path) {
355
+ const normalizedPath = path.replace(/^\/+/, "").replace(/\/+$/, "");
356
+ return this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
357
+ }
358
+ /**
359
+ * Generate a unique ID for a GCS object
360
+ */
361
+ generateId(key) {
362
+ return `gs://${this.options.bucket}/${key}`;
363
+ }
364
+ /**
365
+ * Invalidate caches for a given path
366
+ */
367
+ invalidateCache(path) {
368
+ if (this.statCache) {
369
+ const statKey = createCacheKey(this.options.bucket, this.options.prefix ?? "", path);
370
+ this.statCache.delete(statKey);
371
+ }
372
+ if (this.listCache) {
373
+ const parentPath = path.split("/").slice(0, -1).join("/") || "/";
374
+ const listPrefix = createCacheKey(this.options.bucket, this.options.prefix ?? "", parentPath);
375
+ this.listCache.deleteByPrefix(listPrefix);
376
+ }
377
+ }
378
+ /**
379
+ * Clear all caches
380
+ */
381
+ clearCache() {
382
+ this.listCache?.clear();
383
+ this.statCache?.clear();
384
+ }
385
+ async listHandler(ctx) {
386
+ try {
387
+ const normalizedPath = (ctx.params.path ?? "").replace(/^\/+/, "").replace(/\/+$/, "");
388
+ const fullPrefix = this.options.prefix ? normalizedPath ? `${this.options.prefix}/${normalizedPath}/` : `${this.options.prefix}/` : normalizedPath ? `${normalizedPath}/` : "";
389
+ const maxChildren = ctx.options?.limit ?? 1e3;
390
+ if (this.listCache) {
391
+ const cacheKey = createCacheKey(this.options.bucket, this.options.prefix ?? "", normalizedPath, JSON.stringify(ctx.options ?? {}));
392
+ const cached = this.listCache.get(cacheKey);
393
+ if (cached) return cached;
394
+ }
395
+ const result = await this.listWithDelimiter(fullPrefix, normalizedPath, maxChildren);
396
+ if (this.listCache) {
397
+ const cacheKey = createCacheKey(this.options.bucket, this.options.prefix ?? "", normalizedPath, JSON.stringify(ctx.options ?? {}));
398
+ this.listCache.set(cacheKey, result);
399
+ if (this.statCache) for (const entry of result.data) {
400
+ const statKey = createCacheKey(this.options.bucket, this.options.prefix ?? "", entry.path);
401
+ this.statCache.set(statKey, entry);
402
+ }
403
+ }
404
+ return result;
405
+ } catch (error) {
406
+ if (error instanceof AFSNotFoundError) throw error;
407
+ throw mapGCSError(error);
408
+ }
409
+ }
410
+ /**
411
+ * List with delimiter (single level)
412
+ */
413
+ async listWithDelimiter(prefix, basePath, maxChildren) {
414
+ const childEntries = [];
415
+ const bucketName = this.options.bucket;
416
+ const [files, , apiResponse] = await this.bucket.getFiles({
417
+ prefix,
418
+ delimiter: "/",
419
+ maxResults: maxChildren
420
+ });
421
+ const prefixes = apiResponse?.prefixes;
422
+ if (prefixes) for (const dirPrefix of prefixes) {
423
+ if (!dirPrefix) continue;
424
+ const dirName = dirPrefix.slice(prefix.length).replace(/\/$/, "");
425
+ if (!dirName) continue;
426
+ const entryPath = basePath ? `${basePath}/${dirName}` : dirName;
427
+ const normalizedPath = entryPath.startsWith("/") ? entryPath : `/${entryPath}`;
428
+ childEntries.push({
429
+ id: this.generateId(dirPrefix),
430
+ path: normalizedPath,
431
+ metadata: {
432
+ kind: "afs:node",
433
+ childrenCount: void 0,
434
+ platformRef: generatePlatformRef(bucketName, dirPrefix, true)
435
+ }
436
+ });
437
+ if (childEntries.length >= maxChildren) break;
438
+ }
439
+ for (const file of files) {
440
+ if (!file.name) continue;
441
+ if (file.name === prefix) continue;
442
+ if (file.name.endsWith("/")) {
443
+ const dirName = file.name.slice(prefix.length).replace(/\/$/, "");
444
+ if (!dirName) continue;
445
+ const entryPath$1 = basePath ? `${basePath}/${dirName}` : dirName;
446
+ const normalizedPath$1 = entryPath$1.startsWith("/") ? entryPath$1 : `/${entryPath$1}`;
447
+ childEntries.push({
448
+ id: this.generateId(file.name),
449
+ path: normalizedPath$1,
450
+ updatedAt: file.metadata.updated ? new Date(file.metadata.updated) : void 0,
451
+ metadata: {
452
+ kind: "afs:node",
453
+ childrenCount: void 0,
454
+ platformRef: generatePlatformRef(bucketName, file.name, true)
455
+ }
456
+ });
457
+ if (childEntries.length >= maxChildren) break;
458
+ continue;
459
+ }
460
+ const fileName = file.name.slice(prefix.length);
461
+ if (!fileName || fileName.includes("/")) continue;
462
+ const entryPath = basePath ? `${basePath}/${fileName}` : fileName;
463
+ const normalizedPath = entryPath.startsWith("/") ? entryPath : `/${entryPath}`;
464
+ childEntries.push({
465
+ id: this.generateId(file.name),
466
+ path: normalizedPath,
467
+ updatedAt: file.metadata.updated ? new Date(file.metadata.updated) : void 0,
468
+ metadata: {
469
+ size: file.metadata.size ? Number(file.metadata.size) : void 0,
470
+ contentType: file.metadata.contentType,
471
+ lastModified: file.metadata.updated,
472
+ etag: file.metadata.etag,
473
+ storageClass: file.metadata.storageClass,
474
+ platformRef: generatePlatformRef(bucketName, file.name, false)
475
+ }
476
+ });
477
+ if (childEntries.length >= maxChildren) break;
478
+ }
479
+ const selfPath = basePath ? basePath.startsWith("/") ? basePath : `/${basePath}` : "/";
480
+ if (childEntries.length === 0 && basePath) {
481
+ const key = this.options.prefix ? `${this.options.prefix}/${basePath}` : basePath;
482
+ const file = this.bucket.file(key);
483
+ const [fileExists] = await file.exists();
484
+ if (fileExists) {
485
+ const [metadata] = await file.getMetadata();
486
+ return { data: [{
487
+ id: this.generateId(key),
488
+ path: selfPath,
489
+ updatedAt: metadata.updated ? new Date(metadata.updated) : void 0,
490
+ metadata: {
491
+ size: metadata.size ? Number(metadata.size) : void 0,
492
+ contentType: metadata.contentType,
493
+ lastModified: metadata.updated,
494
+ etag: metadata.etag,
495
+ childrenCount: 0,
496
+ platformRef: generatePlatformRef(bucketName, key, false)
497
+ }
498
+ }] };
499
+ }
500
+ const [dirExists] = await this.bucket.file(`${key}/`).exists();
501
+ if (!dirExists) throw new AFSNotFoundError(selfPath);
502
+ }
503
+ return { data: [{
504
+ id: this.generateId(prefix || "/"),
505
+ path: selfPath,
506
+ metadata: {
507
+ kind: "afs:node",
508
+ childrenCount: childEntries.length,
509
+ platformRef: generatePlatformRef(bucketName, prefix, true)
510
+ }
511
+ }, ...childEntries] };
512
+ }
513
+ async listVersionsHandler(ctx) {
514
+ try {
515
+ const normalizedPath = ctx.params.path.replace(/^\/+/, "").replace(/\/+$/, "");
516
+ const key = this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
517
+ const [files] = await this.bucket.getFiles({
518
+ prefix: key,
519
+ versions: true
520
+ });
521
+ const entries = [];
522
+ for (const versionFile of files) {
523
+ if (versionFile.name !== key) continue;
524
+ const generation = versionFile.metadata.generation;
525
+ if (!generation) continue;
526
+ const versionPath = `/${normalizedPath}/@versions/${generation}`;
527
+ const isLive = !versionFile.metadata.timeDeleted;
528
+ entries.push({
529
+ id: `${this.generateId(key)}:${generation}`,
530
+ path: versionPath,
531
+ updatedAt: versionFile.metadata.timeCreated ? new Date(versionFile.metadata.timeCreated) : void 0,
532
+ metadata: {
533
+ generation,
534
+ isLive,
535
+ timeCreated: versionFile.metadata.timeCreated,
536
+ size: versionFile.metadata.size ? Number(versionFile.metadata.size) : 0,
537
+ etag: versionFile.metadata.etag
538
+ }
539
+ });
540
+ }
541
+ return { data: entries };
542
+ } catch (error) {
543
+ throw mapGCSError(error);
544
+ }
545
+ }
546
+ async readHandler(ctx) {
547
+ try {
548
+ const key = this.buildGCSKey(ctx.params.path);
549
+ const normalizedOutputPath = ctx.path.startsWith("/") ? ctx.path : `/${ctx.path}`;
550
+ const bucketName = this.options.bucket;
551
+ if (!key) {
552
+ const [files$1, , apiResponse$1] = await this.bucket.getFiles({
553
+ prefix: this.options.prefix ? `${this.options.prefix}/` : "",
554
+ delimiter: "/",
555
+ maxResults: 1e3
556
+ });
557
+ const childrenCount = ((apiResponse$1?.prefixes)?.length ?? 0) + files$1.length;
558
+ return {
559
+ id: this.generateId("/"),
560
+ path: "/",
561
+ content: "",
562
+ metadata: {
563
+ kind: "afs:node",
564
+ childrenCount,
565
+ platformRef: generatePlatformRef(bucketName, "", true)
566
+ }
567
+ };
568
+ }
569
+ const file = this.bucket.file(key);
570
+ const [exists] = await file.exists();
571
+ if (exists) {
572
+ const [metadata] = await file.getMetadata();
573
+ if (key.endsWith("/") || metadata.contentType === "application/x-directory") return {
574
+ id: this.generateId(key),
575
+ path: normalizedOutputPath,
576
+ content: "",
577
+ updatedAt: metadata.updated ? new Date(metadata.updated) : void 0,
578
+ metadata: {
579
+ kind: "afs:node",
580
+ childrenCount: void 0,
581
+ platformRef: generatePlatformRef(bucketName, key, true)
582
+ }
583
+ };
584
+ const [content] = await file.download();
585
+ return {
586
+ id: this.generateId(key),
587
+ path: normalizedOutputPath,
588
+ content: content.toString("utf-8"),
589
+ updatedAt: metadata.updated ? new Date(metadata.updated) : void 0,
590
+ metadata: {
591
+ size: metadata.size ? Number(metadata.size) : void 0,
592
+ mimeType: metadata.contentType,
593
+ contentType: metadata.contentType,
594
+ lastModified: metadata.updated,
595
+ etag: metadata.etag
596
+ }
597
+ };
598
+ }
599
+ const [files, , apiResponse] = await this.bucket.getFiles({
600
+ prefix: `${key}/`,
601
+ delimiter: "/",
602
+ maxResults: 1e3
603
+ });
604
+ const prefixes = apiResponse?.prefixes;
605
+ if (files.length > 0 || prefixes && prefixes.length > 0) {
606
+ const childrenCount = (prefixes?.length ?? 0) + files.length;
607
+ return {
608
+ id: this.generateId(`${key}/`),
609
+ path: normalizedOutputPath,
610
+ content: "",
611
+ metadata: {
612
+ kind: "afs:node",
613
+ childrenCount,
614
+ platformRef: generatePlatformRef(bucketName, key, true)
615
+ }
616
+ };
617
+ }
618
+ const dirMarker = this.bucket.file(`${key}/`);
619
+ const [dirExists] = await dirMarker.exists();
620
+ if (dirExists) {
621
+ const [dirMetadata] = await dirMarker.getMetadata();
622
+ return {
623
+ id: this.generateId(`${key}/`),
624
+ path: normalizedOutputPath,
625
+ content: "",
626
+ updatedAt: dirMetadata.updated ? new Date(dirMetadata.updated) : void 0,
627
+ metadata: {
628
+ kind: "afs:node",
629
+ childrenCount: void 0,
630
+ platformRef: generatePlatformRef(bucketName, key, true)
631
+ }
632
+ };
633
+ }
634
+ throw new AFSNotFoundError(`/${ctx.params.path}`);
635
+ } catch (error) {
636
+ if (error instanceof AFSError || error instanceof AFSNotFoundError) throw error;
637
+ throw mapGCSError(error);
638
+ }
639
+ }
640
+ async readVersionHandler(ctx) {
641
+ try {
642
+ const normalizedPath = ctx.params.path.replace(/^\/+/, "").replace(/\/+$/, "");
643
+ const key = this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
644
+ const generation = ctx.params.generation;
645
+ const file = this.bucket.file(key, { generation: parseInt(generation, 10) });
646
+ const [exists] = await file.exists();
647
+ if (!exists) throw new AFSNotFoundError(`/${normalizedPath}/@versions/${generation}`);
648
+ const [content] = await file.download();
649
+ const [metadata] = await file.getMetadata();
650
+ return {
651
+ id: `${this.generateId(key)}:${generation}`,
652
+ path: ctx.path,
653
+ content: content.toString("utf-8"),
654
+ updatedAt: metadata.timeCreated ? new Date(metadata.timeCreated) : void 0,
655
+ metadata: {
656
+ size: metadata.size ? Number(metadata.size) : void 0,
657
+ contentType: metadata.contentType,
658
+ timeCreated: metadata.timeCreated,
659
+ etag: metadata.etag,
660
+ generation: metadata.generation
661
+ }
662
+ };
663
+ } catch (error) {
664
+ if (error instanceof AFSNotFoundError) throw error;
665
+ throw mapGCSError(error);
666
+ }
667
+ }
668
+ async metaHandler(ctx) {
669
+ try {
670
+ const path = ctx.params.path ?? "";
671
+ const normalizedPath = path.replace(/^\/+/, "").replace(/\/+$/, "");
672
+ const key = this.options.prefix ? normalizedPath ? `${this.options.prefix}/${normalizedPath}` : this.options.prefix : normalizedPath;
673
+ const bucketName = this.options.bucket;
674
+ if (!key) {
675
+ const [files$1, , apiResponse] = await this.bucket.getFiles({
676
+ prefix: this.options.prefix ? `${this.options.prefix}/` : "",
677
+ delimiter: "/",
678
+ maxResults: 1e3
679
+ });
680
+ const childrenCount = ((apiResponse?.prefixes)?.length ?? 0) + files$1.length;
681
+ return {
682
+ id: this.generateId("/"),
683
+ path: "/.meta",
684
+ metadata: {
685
+ kind: "afs:node",
686
+ childrenCount,
687
+ platformRef: generatePlatformRef(bucketName, "", true)
688
+ }
689
+ };
690
+ }
691
+ if (this.statCache) {
692
+ const cacheKey = createCacheKey(bucketName, this.options.prefix ?? "", path);
693
+ const cached = this.statCache.get(cacheKey);
694
+ if (cached) return {
695
+ id: cached.id,
696
+ path: ctx.path,
697
+ metadata: cached.metadata
698
+ };
699
+ }
700
+ const file = this.bucket.file(key);
701
+ const [exists] = await file.exists();
702
+ if (exists) {
703
+ const [metadata] = await file.getMetadata();
704
+ if (key.endsWith("/") || metadata.contentType === "application/x-directory") return {
705
+ id: this.generateId(key),
706
+ path: ctx.path,
707
+ updatedAt: metadata.updated ? new Date(metadata.updated) : void 0,
708
+ metadata: {
709
+ kind: "afs:node",
710
+ childrenCount: void 0,
711
+ platformRef: generatePlatformRef(bucketName, key, true)
712
+ }
713
+ };
714
+ const result = {
715
+ id: this.generateId(key),
716
+ path: ctx.path,
717
+ updatedAt: metadata.updated ? new Date(metadata.updated) : void 0,
718
+ metadata: {
719
+ kind: "afs:document",
720
+ size: metadata.size ? Number(metadata.size) : void 0,
721
+ contentType: metadata.contentType,
722
+ lastModified: metadata.updated,
723
+ etag: metadata.etag,
724
+ storageClass: metadata.storageClass,
725
+ generation: metadata.generation,
726
+ metageneration: metadata.metageneration,
727
+ crc32c: metadata.crc32c,
728
+ md5Hash: metadata.md5Hash,
729
+ platformRef: generatePlatformRef(bucketName, key, false)
730
+ }
731
+ };
732
+ if (this.statCache) {
733
+ const cacheKey = createCacheKey(bucketName, this.options.prefix ?? "", path);
734
+ this.statCache.set(cacheKey, result);
735
+ }
736
+ return result;
737
+ }
738
+ const [files] = await this.bucket.getFiles({
739
+ prefix: `${key}/`,
740
+ maxResults: 1
741
+ });
742
+ if (files.length > 0) return {
743
+ id: this.generateId(`${key}/`),
744
+ path: ctx.path,
745
+ metadata: {
746
+ kind: "afs:node",
747
+ childrenCount: void 0,
748
+ platformRef: generatePlatformRef(bucketName, key, true)
749
+ }
750
+ };
751
+ const dirMarker = this.bucket.file(`${key}/`);
752
+ const [dirExists] = await dirMarker.exists();
753
+ if (dirExists) {
754
+ const [dirMetadata] = await dirMarker.getMetadata();
755
+ return {
756
+ id: this.generateId(`${key}/`),
757
+ path: ctx.path,
758
+ updatedAt: dirMetadata.updated ? new Date(dirMetadata.updated) : void 0,
759
+ metadata: {
760
+ kind: "afs:node",
761
+ childrenCount: void 0,
762
+ platformRef: generatePlatformRef(bucketName, key, true)
763
+ }
764
+ };
765
+ }
766
+ throw new AFSNotFoundError(path.startsWith("/") ? path : `/${path}`);
767
+ } catch (error) {
768
+ if (error instanceof AFSNotFoundError) throw error;
769
+ throw mapGCSError(error);
770
+ }
771
+ }
772
+ async statHandler(ctx) {
773
+ const metaEntry = await this.metaHandler({
774
+ ...ctx,
775
+ path: ctx.path.endsWith("/.meta") ? ctx.path : `${ctx.path}/.meta`
776
+ });
777
+ return { data: {
778
+ path: ctx.path,
779
+ size: metaEntry.metadata?.size,
780
+ childrenCount: metaEntry.metadata?.childrenCount,
781
+ meta: metaEntry.metadata
782
+ } };
783
+ }
784
+ async writeHandler(ctx, payload) {
785
+ try {
786
+ const key = this.buildGCSKey(ctx.params.path);
787
+ const file = this.bucket.file(key);
788
+ let content;
789
+ if (typeof payload.content === "string") content = payload.content;
790
+ else if (Buffer.isBuffer(payload.content)) content = payload.content;
791
+ else if (payload.content !== void 0) content = JSON.stringify(payload.content);
792
+ else content = "";
793
+ const saveOptions = { validation: false };
794
+ if (payload.metadata?.mimeType || payload.metadata?.contentType) saveOptions.contentType = payload.metadata.mimeType ?? payload.metadata.contentType;
795
+ await file.save(content, saveOptions);
796
+ const [metadata] = await file.getMetadata();
797
+ const normalizedOutputPath = ctx.path.startsWith("/") ? ctx.path : `/${ctx.path}`;
798
+ this.invalidateCache(ctx.params.path);
799
+ return { data: {
800
+ id: this.generateId(key),
801
+ path: normalizedOutputPath,
802
+ content: payload.content,
803
+ updatedAt: metadata.updated ? new Date(metadata.updated) : /* @__PURE__ */ new Date(),
804
+ metadata: {
805
+ size: metadata.size ? Number(metadata.size) : 0,
806
+ etag: metadata.etag,
807
+ generation: metadata.generation,
808
+ ...payload.metadata
809
+ }
810
+ } };
811
+ } catch (error) {
812
+ throw mapGCSError(error);
813
+ }
814
+ }
815
+ async deleteHandler(ctx) {
816
+ try {
817
+ const key = this.buildGCSKey(ctx.params.path);
818
+ const file = this.bucket.file(key);
819
+ const [exists] = await file.exists();
820
+ if (exists) {
821
+ await file.delete();
822
+ this.invalidateCache(ctx.params.path);
823
+ return { message: `Successfully deleted: ${ctx.params.path}` };
824
+ }
825
+ const [files] = await this.bucket.getFiles({
826
+ prefix: `${key}/`,
827
+ maxResults: 1
828
+ });
829
+ if (files.length === 0) throw new AFSNotFoundError(`/${ctx.params.path}`);
830
+ throw new AFSError(`Cannot delete non-empty directory: ${ctx.params.path}. Use recursive option.`, "AFS_INVALID_OPERATION");
831
+ } catch (error) {
832
+ if (error instanceof AFSError || error instanceof AFSNotFoundError) throw error;
833
+ throw mapGCSError(error);
834
+ }
835
+ }
836
+ async listActionsHandler(ctx) {
837
+ const basePath = ctx.path.replace(/\/\.actions$/, "");
838
+ return { data: [
839
+ {
840
+ id: "presign-download",
841
+ path: `${basePath}/.actions/presign-download`,
842
+ summary: "Generate signed download URL",
843
+ metadata: {
844
+ kind: "afs:executable",
845
+ kinds: ["afs:executable", "afs:node"]
846
+ }
847
+ },
848
+ {
849
+ id: "presign-upload",
850
+ path: `${basePath}/.actions/presign-upload`,
851
+ summary: "Generate signed upload URL",
852
+ metadata: {
853
+ kind: "afs:executable",
854
+ kinds: ["afs:executable", "afs:node"]
855
+ }
856
+ },
857
+ {
858
+ id: "compose",
859
+ path: `${basePath}/.actions/compose`,
860
+ summary: "Compose multiple objects into one",
861
+ metadata: {
862
+ kind: "afs:executable",
863
+ kinds: ["afs:executable", "afs:node"]
864
+ }
865
+ },
866
+ {
867
+ id: "rewrite",
868
+ path: `${basePath}/.actions/rewrite`,
869
+ summary: "Rewrite object to new location",
870
+ metadata: {
871
+ kind: "afs:executable",
872
+ kinds: ["afs:executable", "afs:node"]
873
+ }
874
+ }
875
+ ] };
876
+ }
877
+ async signDownloadActionHandler(ctx, args) {
878
+ const normalizedPath = ctx.params.path.replace(/^\/+/, "").replace(/\/+$/, "");
879
+ const key = this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
880
+ const file = this.bucket.file(key);
881
+ let expiresIn = args.expiresIn ?? DEFAULT_EXPIRES_IN;
882
+ if (expiresIn > MAX_EXPIRES_IN) expiresIn = MAX_EXPIRES_IN;
883
+ if (expiresIn < 1) expiresIn = 1;
884
+ const expiresAt = Date.now() + expiresIn * 1e3;
885
+ const [url] = await file.getSignedUrl({
886
+ action: "read",
887
+ expires: expiresAt
888
+ });
889
+ return {
890
+ success: true,
891
+ data: {
892
+ url,
893
+ expiresAt: new Date(expiresAt).toISOString()
894
+ }
895
+ };
896
+ }
897
+ async signUploadActionHandler(ctx, args) {
898
+ const normalizedPath = ctx.params.path.replace(/^\/+/, "").replace(/\/+$/, "");
899
+ const key = this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
900
+ const file = this.bucket.file(key);
901
+ let expiresIn = args.expiresIn ?? DEFAULT_EXPIRES_IN;
902
+ if (expiresIn > MAX_EXPIRES_IN) expiresIn = MAX_EXPIRES_IN;
903
+ if (expiresIn < 1) expiresIn = 1;
904
+ const expiresAt = Date.now() + expiresIn * 1e3;
905
+ const contentType = args.contentType ?? "application/octet-stream";
906
+ const [url] = await file.getSignedUrl({
907
+ action: "write",
908
+ expires: expiresAt,
909
+ contentType
910
+ });
911
+ return {
912
+ success: true,
913
+ data: {
914
+ url,
915
+ expiresAt: new Date(expiresAt).toISOString()
916
+ }
917
+ };
918
+ }
919
+ async composeActionHandler(ctx, args) {
920
+ const normalizedPath = ctx.params.path.replace(/^\/+/, "").replace(/\/+$/, "");
921
+ const destinationKey = this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
922
+ const sources = args.sources;
923
+ if (!sources || !Array.isArray(sources) || sources.length < 2) throw new AFSError("compose requires at least 2 source objects", "AFS_INVALID_ARGUMENT");
924
+ if (sources.length > MAX_COMPOSE_SOURCES) throw new AFSError(`compose supports maximum ${MAX_COMPOSE_SOURCES} sources`, "AFS_INVALID_ARGUMENT");
925
+ const sourceFiles = sources.map((source) => {
926
+ const sourcePath = source.replace(/^\/+/, "").replace(/\/+$/, "");
927
+ const sourceKey = this.options.prefix ? `${this.options.prefix}/${sourcePath}` : sourcePath;
928
+ return this.bucket.file(sourceKey);
929
+ });
930
+ const destinationFile = this.bucket.file(destinationKey);
931
+ await this.bucket.combine(sourceFiles, destinationFile);
932
+ const [metadata] = await destinationFile.getMetadata();
933
+ this.invalidateCache(ctx.params.path);
934
+ return {
935
+ success: true,
936
+ data: {
937
+ generation: metadata.generation,
938
+ size: metadata.size ? Number(metadata.size) : 0,
939
+ etag: metadata.etag
940
+ }
941
+ };
942
+ }
943
+ async rewriteActionHandler(ctx, args) {
944
+ const normalizedPath = ctx.params.path.replace(/^\/+/, "").replace(/\/+$/, "");
945
+ const sourceKey = this.options.prefix ? `${this.options.prefix}/${normalizedPath}` : normalizedPath;
946
+ const destination = args.destination;
947
+ if (!destination) throw new AFSError("rewrite requires destination path", "AFS_INVALID_ARGUMENT");
948
+ const destPath = destination.replace(/^\/+/, "").replace(/\/+$/, "");
949
+ const destKey = this.options.prefix ? `${this.options.prefix}/${destPath}` : destPath;
950
+ const sourceFile = this.bucket.file(sourceKey);
951
+ const destFile = this.bucket.file(destKey);
952
+ const copyOptions = {};
953
+ if (args.storageClass) copyOptions.metadata = { storageClass: args.storageClass };
954
+ if (args.contentType) copyOptions.contentType = args.contentType;
955
+ await sourceFile.copy(destFile, copyOptions);
956
+ const [metadata] = await destFile.getMetadata();
957
+ this.invalidateCache(destPath);
958
+ return {
959
+ success: true,
960
+ data: {
961
+ destination: `/${destPath}`,
962
+ generation: metadata.generation,
963
+ size: metadata.size ? Number(metadata.size) : 0,
964
+ etag: metadata.etag,
965
+ storageClass: metadata.storageClass
966
+ }
967
+ };
968
+ }
969
+ /**
970
+ * Generate a signed URL for downloading an object
971
+ * @deprecated Use action /.actions/sign-download instead
972
+ */
973
+ async getSignedDownloadUrl(path, options) {
974
+ return (await this.signDownloadActionHandler({
975
+ path: `/${path}`,
976
+ params: { path },
977
+ options: {}
978
+ }, { expiresIn: options?.expiresIn })).data.url;
979
+ }
980
+ /**
981
+ * Generate a signed URL for uploading an object
982
+ * @deprecated Use action /.actions/sign-upload instead
983
+ */
984
+ async getSignedUploadUrl(path, options) {
985
+ return (await this.signUploadActionHandler({
986
+ path: `/${path}`,
987
+ params: { path },
988
+ options: {}
989
+ }, {
990
+ expiresIn: options?.expiresIn,
991
+ contentType: options?.contentType
992
+ })).data.url;
993
+ }
994
+ /**
995
+ * List all versions (generations) of an object
996
+ * @deprecated Use list on /:path/@versions instead
997
+ */
998
+ async listVersions(path) {
999
+ return (await this.listVersionsHandler({
1000
+ path: `/${path}/@versions`,
1001
+ params: { path },
1002
+ options: {}
1003
+ })).data.map((entry) => ({
1004
+ generation: entry.metadata?.generation,
1005
+ isLive: entry.metadata?.isLive,
1006
+ timeCreated: entry.updatedAt,
1007
+ size: entry.metadata?.size ?? 0,
1008
+ etag: entry.metadata?.etag
1009
+ }));
1010
+ }
1011
+ /**
1012
+ * Read a specific version (generation) of an object
1013
+ * @deprecated Use read on /:path/@versions/:generation instead
1014
+ */
1015
+ async readVersion(path, generation) {
1016
+ const result = await this.readVersionHandler({
1017
+ path: `/${path}/@versions/${generation}`,
1018
+ params: {
1019
+ path,
1020
+ generation
1021
+ },
1022
+ options: {}
1023
+ });
1024
+ return {
1025
+ content: result.content,
1026
+ metadata: result.metadata
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Create a directory marker
1031
+ * @deprecated Use write with empty content instead
1032
+ */
1033
+ async mkdir(path) {
1034
+ const key = this.buildGCSKey(path);
1035
+ const dirKey = key.endsWith("/") ? key : `${key}/`;
1036
+ await this.bucket.file(dirKey).save("", { contentType: "application/x-directory" });
1037
+ }
1038
+ };
1039
+ __decorate([List("/"), List("/:path*")], AFSGCS.prototype, "listHandler", null);
1040
+ __decorate([List("/:path*/@versions")], AFSGCS.prototype, "listVersionsHandler", null);
1041
+ __decorate([Read("/:path*")], AFSGCS.prototype, "readHandler", null);
1042
+ __decorate([Read("/:path*/@versions/:generation")], AFSGCS.prototype, "readVersionHandler", null);
1043
+ __decorate([Meta("/"), Meta("/:path*")], AFSGCS.prototype, "metaHandler", null);
1044
+ __decorate([Stat("/"), Stat("/:path*")], AFSGCS.prototype, "statHandler", null);
1045
+ __decorate([Write("/:path*")], AFSGCS.prototype, "writeHandler", null);
1046
+ __decorate([Delete("/:path*")], AFSGCS.prototype, "deleteHandler", null);
1047
+ __decorate([Actions("/:path*")], AFSGCS.prototype, "listActionsHandler", null);
1048
+ __decorate([Actions.Exec("/:path*", "presign-download")], AFSGCS.prototype, "signDownloadActionHandler", null);
1049
+ __decorate([Actions.Exec("/:path*", "presign-upload")], AFSGCS.prototype, "signUploadActionHandler", null);
1050
+ __decorate([Actions.Exec("/:path*", "compose")], AFSGCS.prototype, "composeActionHandler", null);
1051
+ __decorate([Actions.Exec("/:path*", "rewrite")], AFSGCS.prototype, "rewriteActionHandler", null);
1052
+
1053
+ //#endregion
1054
+ export { AFSGCS };
1055
+ //# sourceMappingURL=index.mjs.map