@aws-blocks/bb-file-bucket 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +174 -0
  2. package/README.md +197 -0
  3. package/dist/bucket-name.d.ts +9 -0
  4. package/dist/bucket-name.d.ts.map +1 -0
  5. package/dist/bucket-name.js +62 -0
  6. package/dist/bucket-name.test.d.ts +2 -0
  7. package/dist/bucket-name.test.d.ts.map +1 -0
  8. package/dist/bucket-name.test.js +61 -0
  9. package/dist/errors.d.ts +20 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +21 -0
  12. package/dist/file-server.d.ts +14 -0
  13. package/dist/file-server.d.ts.map +1 -0
  14. package/dist/file-server.js +169 -0
  15. package/dist/file-server.test.d.ts +2 -0
  16. package/dist/file-server.test.d.ts.map +1 -0
  17. package/dist/file-server.test.js +307 -0
  18. package/dist/index.aws.d.ts +47 -0
  19. package/dist/index.aws.d.ts.map +1 -0
  20. package/dist/index.aws.js +183 -0
  21. package/dist/index.browser.d.ts +4 -0
  22. package/dist/index.browser.d.ts.map +1 -0
  23. package/dist/index.browser.js +6 -0
  24. package/dist/index.cdk.d.ts +16 -0
  25. package/dist/index.cdk.d.ts.map +1 -0
  26. package/dist/index.cdk.js +75 -0
  27. package/dist/index.cdk.test.d.ts +2 -0
  28. package/dist/index.cdk.test.d.ts.map +1 -0
  29. package/dist/index.cdk.test.js +69 -0
  30. package/dist/index.mock.d.ts +246 -0
  31. package/dist/index.mock.d.ts.map +1 -0
  32. package/dist/index.mock.js +502 -0
  33. package/dist/index.test.d.ts +2 -0
  34. package/dist/index.test.d.ts.map +1 -0
  35. package/dist/index.test.js +318 -0
  36. package/dist/middleware.d.ts +3 -0
  37. package/dist/middleware.d.ts.map +1 -0
  38. package/dist/middleware.js +62 -0
  39. package/dist/mock-middleware.d.ts +3 -0
  40. package/dist/mock-middleware.d.ts.map +1 -0
  41. package/dist/mock-middleware.js +62 -0
  42. package/dist/mock-utils.d.ts +11 -0
  43. package/dist/mock-utils.d.ts.map +1 -0
  44. package/dist/mock-utils.js +28 -0
  45. package/dist/path-containment.test.d.ts +2 -0
  46. package/dist/path-containment.test.d.ts.map +1 -0
  47. package/dist/path-containment.test.js +91 -0
  48. package/dist/paths.d.ts +25 -0
  49. package/dist/paths.d.ts.map +1 -0
  50. package/dist/paths.js +67 -0
  51. package/dist/scan.test.d.ts +2 -0
  52. package/dist/scan.test.d.ts.map +1 -0
  53. package/dist/scan.test.js +107 -0
  54. package/dist/tokens.d.ts +12 -0
  55. package/dist/tokens.d.ts.map +1 -0
  56. package/dist/tokens.js +42 -0
  57. package/dist/types.d.ts +170 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +3 -0
  60. package/dist/url-encoding.test.d.ts +2 -0
  61. package/dist/url-encoding.test.d.ts.map +1 -0
  62. package/dist/url-encoding.test.js +88 -0
  63. package/dist/version.d.ts +3 -0
  64. package/dist/version.d.ts.map +1 -0
  65. package/dist/version.js +3 -0
  66. package/package.json +57 -0
  67. package/src/bucket-name.test.ts +103 -0
  68. package/src/bucket-name.ts +83 -0
  69. package/src/errors.ts +22 -0
  70. package/src/file-server.test.ts +381 -0
  71. package/src/file-server.ts +203 -0
  72. package/src/index.aws.ts +219 -0
  73. package/src/index.browser.ts +7 -0
  74. package/src/index.cdk.test.ts +84 -0
  75. package/src/index.cdk.ts +89 -0
  76. package/src/index.mock.ts +531 -0
  77. package/src/index.test.ts +366 -0
  78. package/src/middleware.ts +66 -0
  79. package/src/mock-middleware.ts +66 -0
  80. package/src/mock-utils.ts +31 -0
  81. package/src/path-containment.test.ts +122 -0
  82. package/src/paths.ts +78 -0
  83. package/src/scan.test.ts +137 -0
  84. package/src/tokens.ts +61 -0
  85. package/src/types.ts +206 -0
  86. package/src/url-encoding.test.ts +120 -0
  87. package/src/version.ts +3 -0
@@ -0,0 +1,502 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Scope, registerSdkIdentifiers } from '@aws-blocks/core';
4
+ import { getMockDataDir } from '@aws-blocks/core/bb-utils';
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'node:fs';
6
+ import { dirname, join, relative } from 'node:path';
7
+ import { assertContainedPath } from './mock-utils.js';
8
+ import { contentRoot, contentPath, metaPath, versionsDirFor, versionContentPath, versionMetaPath, deleteMarkerPath, isVersionEntry, } from './paths.js';
9
+ import { mintFileToken, LOCAL_FILE_SECRET } from './tokens.js';
10
+ import { validateBucketName } from './bucket-name.js';
11
+ import { Logger } from '@aws-blocks/bb-logger';
12
+ import { BB_NAME, BB_VERSION } from './version.js';
13
+ export { FileBucketErrors } from './errors.js';
14
+ // ── Helpers ─────────────────────────────────────────────────────────────────
15
+ const MAX_KEY_BYTES = 1024; // S3 key limit
16
+ function blocksError(name, message) {
17
+ const err = new Error(`${name}: ${message}`);
18
+ err.name = name;
19
+ return err;
20
+ }
21
+ function getDevBaseUrl() {
22
+ const port = globalThis.__BLOCKS_DEV_SERVER_PORT__ ?? 3001;
23
+ return `http://localhost:${port}`;
24
+ }
25
+ // ── FileBucket (mock) ───────────────────────────────────────────────────────
26
+ /**
27
+ * File storage backed by Amazon S3.
28
+ *
29
+ * **When to use:** You need to store, retrieve, or serve binary files —
30
+ * user uploads, generated reports, images, videos, or static assets.
31
+ *
32
+ * **When NOT to use:** If you need structured key-value data with conditional
33
+ * writes, use `KVStore`. If you need queryable records with indexes, use
34
+ * `DistributedTable`.
35
+ *
36
+ * **Best practices:**
37
+ * - Use path prefixes to organize files (e.g., `uploads/{userId}/`, `reports/`)
38
+ * - Set `contentType` on `put()` to ensure correct MIME handling on download
39
+ * - Use presigned URLs (`getUrl` / `putUrl`) for direct browser upload/download
40
+ * - Use `getFileHandle` / `createUploadHandle` for ergonomic browser file transfers
41
+ * - Prefer `scan({ prefix })` over unscoped `scan()` to limit enumeration cost
42
+ *
43
+ * **Scaling:** S3 scales automatically. No provisioned throughput. Costs are
44
+ * per-request plus storage. Individual objects up to 5 TB. For objects larger
45
+ * than ~100 MB, consider multipart upload.
46
+ */
47
+ export class FileBucket extends Scope {
48
+ dataDir;
49
+ versioned;
50
+ /** @internal Logger for internal operations. Defaults to error-level when not provided. */
51
+ log;
52
+ constructor(scope, id, options) {
53
+ super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
54
+ // Validate the derived bucket name against S3's naming rules so local
55
+ // dev fails the same way a deploy would. Validate `fullId` (the real
56
+ // deployed bucket name), not the `mock-` prefixed local name, to keep
57
+ // parity with the CDK path.
58
+ if (!options?.bucket)
59
+ validateBucketName(this.fullId);
60
+ this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
61
+ this.dataDir = getMockDataDir(this);
62
+ this.versioned = options?.versioned ?? false;
63
+ this.registerClientMiddleware('@aws-blocks/bb-file-bucket/middleware');
64
+ this.registerDevAttachment('@aws-blocks/bb-file-bucket/file-server');
65
+ registerSdkIdentifiers(this.fullId, { bucketName: `mock-${this.fullId}` });
66
+ // Register in global registry so the file-server can delegate PUT to bucket.put()
67
+ const registry = (globalThis.__BLOCKS_FILE_BUCKET_REGISTRY__ ??= new Map());
68
+ registry.set(this.fullId, this);
69
+ }
70
+ /**
71
+ * Upload a file.
72
+ *
73
+ * Overwrites any existing file at the given path.
74
+ *
75
+ * @param path - The object key (e.g., `uploads/photo.jpg`).
76
+ * @param body - The file content.
77
+ * @param options - Optional metadata and content settings.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * await bucket.put('reports/q1.pdf', pdfBuffer, {
82
+ * contentType: 'application/pdf',
83
+ * });
84
+ * ```
85
+ */
86
+ async put(path, body, options) {
87
+ this.validateKey(path);
88
+ const filePath = contentPath(this.dataDir, path);
89
+ mkdirSync(dirname(filePath), { recursive: true });
90
+ const buf = typeof body === 'string' ? Buffer.from(body) : body;
91
+ const meta = {
92
+ contentType: options?.contentType ?? 'application/octet-stream',
93
+ metadata: options?.metadata ?? {},
94
+ cacheControl: options?.cacheControl,
95
+ };
96
+ if (this.versioned) {
97
+ const versionId = this.nextVersionId(path);
98
+ const versionsDir = versionsDirFor(this.dataDir, path);
99
+ mkdirSync(versionsDir, { recursive: true });
100
+ writeFileSync(versionContentPath(this.dataDir, path, versionId), buf);
101
+ writeFileSync(versionMetaPath(this.dataDir, path, versionId), JSON.stringify(meta));
102
+ // Remove any delete marker
103
+ try {
104
+ unlinkSync(deleteMarkerPath(this.dataDir, path));
105
+ }
106
+ catch { }
107
+ }
108
+ this.writeMeta(path, meta);
109
+ writeFileSync(filePath, buf);
110
+ }
111
+ /**
112
+ * Download a file.
113
+ *
114
+ * @param path - The object key.
115
+ * @param options - Optional. On versioned buckets, pass `{ versionId }` to retrieve a specific version.
116
+ * @returns The file content and metadata, or null if the file does not exist.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const file = await bucket.get('reports/q1.pdf');
121
+ * if (file) {
122
+ * console.log(file.contentType); // 'application/pdf'
123
+ * }
124
+ * ```
125
+ */
126
+ async get(path, options) {
127
+ this.validateKey(path);
128
+ const versionId = options?.versionId;
129
+ if (versionId && this.versioned) {
130
+ const vPath = versionContentPath(this.dataDir, path, versionId);
131
+ if (!existsSync(vPath))
132
+ return null;
133
+ const body = readFileSync(vPath);
134
+ const meta = this.readVersionMeta(path, versionId);
135
+ return { body, contentType: meta.contentType, metadata: meta.metadata, size: body.length };
136
+ }
137
+ const filePath = contentPath(this.dataDir, path);
138
+ if (!existsSync(filePath))
139
+ return null;
140
+ // Check for delete marker
141
+ if (this.versioned && existsSync(deleteMarkerPath(this.dataDir, path)))
142
+ return null;
143
+ const body = readFileSync(filePath);
144
+ const meta = this.readMeta(path);
145
+ return {
146
+ body,
147
+ contentType: meta.contentType,
148
+ metadata: meta.metadata,
149
+ size: body.length,
150
+ };
151
+ }
152
+ /**
153
+ * Delete a file.
154
+ *
155
+ * On non-versioned buckets, permanently removes the file.
156
+ * On versioned buckets without `versionId`, places a delete marker (file appears deleted
157
+ * but previous versions are preserved). With `versionId`, permanently removes that version.
158
+ *
159
+ * No-op if the file does not exist (matches S3 behavior).
160
+ *
161
+ * @param path - The object key.
162
+ * @param options - Optional. On versioned buckets, pass `{ versionId }` to permanently delete a specific version.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * await bucket.delete('uploads/old-photo.jpg');
167
+ * ```
168
+ */
169
+ async delete(path, options) {
170
+ this.validateKey(path);
171
+ const versionId = options?.versionId;
172
+ if (this.versioned && versionId) {
173
+ // Permanently delete a specific version
174
+ try {
175
+ unlinkSync(versionContentPath(this.dataDir, path, versionId));
176
+ }
177
+ catch { }
178
+ try {
179
+ unlinkSync(versionMetaPath(this.dataDir, path, versionId));
180
+ }
181
+ catch { }
182
+ return;
183
+ }
184
+ if (this.versioned) {
185
+ // Place a delete marker — current file stays on disk for version history
186
+ const versionsDir = versionsDirFor(this.dataDir, path);
187
+ mkdirSync(versionsDir, { recursive: true });
188
+ writeFileSync(deleteMarkerPath(this.dataDir, path), '');
189
+ return;
190
+ }
191
+ try {
192
+ unlinkSync(contentPath(this.dataDir, path));
193
+ }
194
+ catch { }
195
+ try {
196
+ unlinkSync(metaPath(this.dataDir, path));
197
+ }
198
+ catch { }
199
+ }
200
+ /**
201
+ * Delete multiple files in a single operation.
202
+ *
203
+ * Uses S3 DeleteObjects for efficient bulk deletion (up to 1,000 keys
204
+ * per request). Chunking is handled internally.
205
+ *
206
+ * @param paths - The object keys to delete.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * await bucket.deleteBatch(['tmp/a.txt', 'tmp/b.txt', 'tmp/c.txt']);
211
+ * ```
212
+ */
213
+ async deleteBatch(paths) {
214
+ for (const p of paths) {
215
+ await this.delete(p);
216
+ }
217
+ }
218
+ /**
219
+ * Generate a presigned URL for downloading a file.
220
+ *
221
+ * @param path - The object key.
222
+ * @param options - URL generation options.
223
+ * @returns A presigned URL string.
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * const url = await bucket.getUrl('reports/q1.pdf', { expiresIn: 3600 });
228
+ * ```
229
+ */
230
+ async getUrl(path, options) {
231
+ const expiresIn = options?.expiresIn ?? 3600;
232
+ const token = mintFileToken(this.fullId, path, 'GET', expiresIn, LOCAL_FILE_SECRET);
233
+ const encodedPath = path.split('/').map(s => encodeURIComponent(s)).join('/');
234
+ let url = `${getDevBaseUrl()}/.bb-file-bucket/${this.fullId}/${encodedPath}?token=${token}`;
235
+ const versionId = options?.versionId;
236
+ if (versionId && this.versioned)
237
+ url += `&versionId=${versionId}`;
238
+ return url;
239
+ }
240
+ /**
241
+ * Generate a presigned URL for uploading a file.
242
+ *
243
+ * @param path - The object key.
244
+ * @param options - URL generation options.
245
+ * @returns A presigned URL string.
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const url = await bucket.putUrl('uploads/photo.jpg', {
250
+ * expiresIn: 600,
251
+ * contentType: 'image/jpeg',
252
+ * });
253
+ * ```
254
+ */
255
+ async putUrl(path, options) {
256
+ const expiresIn = options?.expiresIn ?? 3600;
257
+ const token = mintFileToken(this.fullId, path, 'PUT', expiresIn, LOCAL_FILE_SECRET, options?.contentType);
258
+ const encodedPath = path.split('/').map(s => encodeURIComponent(s)).join('/');
259
+ return `${getDevBaseUrl()}/.bb-file-bucket/${this.fullId}/${encodedPath}?token=${token}`;
260
+ }
261
+ /**
262
+ * Get a file handle for browser-side download.
263
+ *
264
+ * Returns a Transferable descriptor that the client middleware hydrates
265
+ * into an object with `.download()` and `.getUrl()` methods. Use this
266
+ * instead of `getUrl()` when you want the client to download files
267
+ * without manually managing presigned URLs.
268
+ *
269
+ * @param path - The object key.
270
+ * @param options - URL generation options.
271
+ * @returns A Transferable file download handle.
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * // Backend
276
+ * async getReport(name: string) {
277
+ * return bucket.getFileHandle('reports/' + name);
278
+ * }
279
+ *
280
+ * // Frontend
281
+ * const file = await api.getReport('q1.pdf');
282
+ * const blob = await file.download();
283
+ * ```
284
+ */
285
+ async getFileHandle(path, options) {
286
+ const url = await this.getUrl(path, options);
287
+ return {
288
+ download: async () => {
289
+ const res = await fetch(url);
290
+ if (!res.ok)
291
+ throw new Error(`Download failed: ${res.status}`);
292
+ return res.blob();
293
+ },
294
+ getUrl: () => url,
295
+ toJSON: () => ({ __blocks: 'file-bucket/download', url }),
296
+ };
297
+ }
298
+ /**
299
+ * Create an upload handle for browser-side file upload.
300
+ *
301
+ * Returns a Transferable descriptor that the client middleware hydrates
302
+ * into an object with `.upload(body)` and `.getUrl()` methods. Use this
303
+ * instead of `putUrl()` when you want the client to upload files
304
+ * without manually managing presigned URLs.
305
+ *
306
+ * @param path - The object key.
307
+ * @param options - URL generation options.
308
+ * @returns A Transferable file upload handle.
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * // Backend
313
+ * async getUploadSlot(name: string) {
314
+ * return bucket.createUploadHandle('uploads/' + name, {
315
+ * contentType: 'image/jpeg',
316
+ * });
317
+ * }
318
+ *
319
+ * // Frontend
320
+ * const slot = await api.getUploadSlot('photo.jpg');
321
+ * await slot.upload(fileBlob);
322
+ * ```
323
+ */
324
+ async createUploadHandle(path, options) {
325
+ const expiresIn = options?.expiresIn ?? 3600;
326
+ const token = mintFileToken(this.fullId, path, 'PUT', expiresIn, LOCAL_FILE_SECRET, options?.contentType);
327
+ const encodedPath = path.split('/').map(s => encodeURIComponent(s)).join('/');
328
+ const url = `${getDevBaseUrl()}/.bb-file-bucket/${this.fullId}/${encodedPath}?token=${token}`;
329
+ const contentType = options?.contentType;
330
+ return {
331
+ upload: async (body) => {
332
+ const headers = {};
333
+ if (contentType)
334
+ headers['Content-Type'] = contentType;
335
+ const res = await fetch(url, { method: 'PUT', body, headers });
336
+ if (!res.ok)
337
+ throw new Error(`Upload failed: ${res.status}`);
338
+ },
339
+ getUrl: () => url,
340
+ toJSON: () => ({ __blocks: 'file-bucket/upload', url, contentType }),
341
+ };
342
+ }
343
+ /**
344
+ * List files in the bucket.
345
+ *
346
+ * Returns an `AsyncIterable` that paginates internally. Use `prefix`
347
+ * to scope the listing. Without a prefix, enumerates all files —
348
+ * this is expensive on large buckets.
349
+ *
350
+ * @param options - Listing options.
351
+ * @returns An async iterable of file info objects.
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * for await (const file of bucket.scan({ prefix: 'uploads/' })) {
356
+ * console.log(file.path, file.size);
357
+ * }
358
+ * ```
359
+ */
360
+ async *scan(options) {
361
+ const root = contentRoot(this.dataDir);
362
+ for (const absPath of this.walkDir(root)) {
363
+ const relPath = relative(root, absPath).replace(/\\/g, '/');
364
+ if (options?.prefix && !relPath.startsWith(options.prefix))
365
+ continue;
366
+ // Skip files with delete markers
367
+ if (this.versioned && existsSync(deleteMarkerPath(this.dataDir, relPath)))
368
+ continue;
369
+ const stat = statSync(absPath);
370
+ yield { path: relPath, size: stat.size, lastModified: stat.mtime };
371
+ }
372
+ }
373
+ /**
374
+ * List all versions of a file. Only available on versioned buckets.
375
+ *
376
+ * Returns versions newest-first. Includes the current version and all
377
+ * previous versions. Does not include delete markers.
378
+ *
379
+ * @param path - The object key.
380
+ * @returns An array of version metadata, newest first.
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * const versions = await bucket.listVersions('report.pdf');
385
+ * for (const v of versions) {
386
+ * console.log(v.versionId, v.lastModified, v.isCurrent);
387
+ * }
388
+ * ```
389
+ */
390
+ async listVersions(path) {
391
+ const versionsDir = versionsDirFor(this.dataDir, path);
392
+ if (!existsSync(versionsDir))
393
+ return [];
394
+ const entries = readdirSync(versionsDir).filter(isVersionEntry);
395
+ if (entries.length === 0)
396
+ return [];
397
+ const hasDeleteMarker = existsSync(deleteMarkerPath(this.dataDir, path));
398
+ const versions = entries.map(versionId => {
399
+ const vPath = versionContentPath(this.dataDir, path, versionId);
400
+ const stat = statSync(vPath);
401
+ return { versionId, lastModified: stat.mtime, size: stat.size, isCurrent: false };
402
+ });
403
+ // Sort newest first; break ties by version number (descending) for deterministic ordering
404
+ versions.sort((a, b) => {
405
+ const timeDiff = b.lastModified.getTime() - a.lastModified.getTime();
406
+ if (timeDiff !== 0)
407
+ return timeDiff;
408
+ const aNum = parseInt(a.versionId.slice(1), 10);
409
+ const bNum = parseInt(b.versionId.slice(1), 10);
410
+ return bNum - aNum;
411
+ });
412
+ // Mark the newest as current (unless there's a delete marker)
413
+ if (versions.length > 0 && !hasDeleteMarker) {
414
+ versions[0].isCurrent = true;
415
+ }
416
+ return versions;
417
+ }
418
+ /**
419
+ * Restore a previous version of a file, making it the current version.
420
+ *
421
+ * Creates a new version that is a copy of the specified old version.
422
+ * On S3, this is implemented as a CopyObject from the old version to the same key.
423
+ *
424
+ * @param path - The object key.
425
+ * @param versionId - The version to restore.
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * const versions = await bucket.listVersions('report.pdf');
430
+ * await bucket.restoreVersion('report.pdf', versions[1].versionId);
431
+ * ```
432
+ */
433
+ async restoreVersion(path, versionId) {
434
+ const vPath = versionContentPath(this.dataDir, path, versionId);
435
+ if (!existsSync(vPath)) {
436
+ throw blocksError('NoSuchVersion', `Version "${versionId}" does not exist for "${path}"`);
437
+ }
438
+ const body = readFileSync(vPath);
439
+ const meta = this.readVersionMeta(path, versionId);
440
+ await this.put(path, body, { contentType: meta.contentType, metadata: meta.metadata });
441
+ }
442
+ /**
443
+ * Create a reference to an existing S3 bucket not managed by this scope.
444
+ *
445
+ * @param bucketName - The name of the existing S3 bucket.
446
+ */
447
+ static fromExisting(bucketName) {
448
+ return { __brand: 'ExternalBucketRef', bucketName };
449
+ }
450
+ // ── Internal helpers ──────────────────────────────────────────────────
451
+ validateKey(key) {
452
+ if (Buffer.byteLength(key, 'utf8') > MAX_KEY_BYTES) {
453
+ this.log.warn(`Key "${key}" exceeds S3's 1,024-byte limit`);
454
+ }
455
+ assertContainedPath(contentRoot(this.dataDir), key);
456
+ }
457
+ writeMeta(path, meta) {
458
+ const mPath = metaPath(this.dataDir, path);
459
+ mkdirSync(dirname(mPath), { recursive: true });
460
+ writeFileSync(mPath, JSON.stringify(meta));
461
+ }
462
+ readMeta(path) {
463
+ return this.readMetaFile(metaPath(this.dataDir, path));
464
+ }
465
+ readVersionMeta(path, versionId) {
466
+ return this.readMetaFile(versionMetaPath(this.dataDir, path, versionId));
467
+ }
468
+ readMetaFile(mPath) {
469
+ if (existsSync(mPath)) {
470
+ try {
471
+ return JSON.parse(readFileSync(mPath, 'utf8'));
472
+ }
473
+ catch { }
474
+ }
475
+ return { contentType: 'application/octet-stream', metadata: {} };
476
+ }
477
+ walkDir(dir) {
478
+ if (!existsSync(dir))
479
+ return [];
480
+ const results = [];
481
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
482
+ const full = join(dir, entry.name);
483
+ if (entry.isDirectory()) {
484
+ results.push(...this.walkDir(full));
485
+ }
486
+ else {
487
+ results.push(full);
488
+ }
489
+ }
490
+ return results;
491
+ }
492
+ nextVersionId(path) {
493
+ const dir = versionsDirFor(this.dataDir, path);
494
+ if (!existsSync(dir))
495
+ return 'v1';
496
+ const existing = readdirSync(dir)
497
+ .filter(f => f.startsWith('v') && !f.includes('.'))
498
+ .map(f => parseInt(f.slice(1), 10))
499
+ .filter(n => !isNaN(n));
500
+ return `v${(existing.length > 0 ? Math.max(...existing) : 0) + 1}`;
501
+ }
502
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}