@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/LICENSE.md +26 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1055 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
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
|