@objectstack/service-storage 4.2.0 → 5.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.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin, PluginContext } from '@objectstack/core';
2
+ import { MetricsRegistry } from '@objectstack/observability';
2
3
  import { IStorageService, StorageUploadOptions, StorageFileInfo, PresignedUploadDescriptor, PresignedDownloadDescriptor, IDataEngine, IHttpServer } from '@objectstack/spec/contracts';
3
4
  import { z } from 'zod';
4
5
  import * as _objectstack_spec_data from '@objectstack/spec/data';
@@ -28,6 +29,8 @@ interface LocalStorageAdapterOptions {
28
29
  * Auto-generated if omitted (suitable for single-process dev usage).
29
30
  */
30
31
  signingSecret?: string;
32
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
33
+ metrics?: MetricsRegistry;
31
34
  }
32
35
  interface PresignTokenPayload {
33
36
  k: string;
@@ -52,7 +55,13 @@ declare class LocalStorageAdapter implements IStorageService {
52
55
  private readonly baseUrl;
53
56
  private readonly basePath;
54
57
  private readonly signingSecret;
58
+ private readonly metrics;
55
59
  constructor(options: LocalStorageAdapterOptions);
60
+ /**
61
+ * Wrap a storage operation with metrics instrumentation. Never swallows
62
+ * the underlying error; instrumentation failures are silently ignored.
63
+ */
64
+ private track;
56
65
  private resolvePath;
57
66
  private resolvePartPath;
58
67
  upload(key: string, data: Buffer | ReadableStream, _options?: StorageUploadOptions): Promise<void>;
@@ -238,6 +247,8 @@ interface S3StorageAdapterOptions {
238
247
  secretAccessKey?: string;
239
248
  /** Force path-style URLs (needed for MinIO / self-hosted) */
240
249
  forcePathStyle?: boolean;
250
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
251
+ metrics?: MetricsRegistry;
241
252
  }
242
253
  /**
243
254
  * S3 storage adapter implementing IStorageService.
@@ -261,8 +272,15 @@ declare class S3StorageAdapter implements IStorageService {
261
272
  private readonly region;
262
273
  private readonly endpoint?;
263
274
  private readonly forcePathStyle;
275
+ private readonly metrics;
264
276
  private clientPromise;
265
277
  constructor(options: S3StorageAdapterOptions);
278
+ /**
279
+ * Wrap a storage operation with metrics instrumentation.
280
+ * Records ok/error counters, a duration histogram, and an error counter
281
+ * keyed by error class on failure. Never swallows the underlying error.
282
+ */
283
+ private track;
266
284
  /**
267
285
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
268
286
  * `@aws-sdk/client-s3` isn't installed.
@@ -711,7 +729,7 @@ declare const SystemFile: Omit<{
711
729
  } | undefined;
712
730
  partitioning?: {
713
731
  enabled: boolean;
714
- strategy: "hash" | "list" | "range";
732
+ strategy: "list" | "hash" | "range";
715
733
  key: string;
716
734
  interval?: string | undefined;
717
735
  } | undefined;
@@ -1025,11 +1043,17 @@ declare const SystemFile: Omit<{
1025
1043
  trash: boolean;
1026
1044
  mru: boolean;
1027
1045
  clone: boolean;
1028
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1046
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1029
1047
  } | undefined;
1030
1048
  recordTypes?: string[] | undefined;
1031
1049
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
1032
1050
  keyPrefix?: string | undefined;
1051
+ detail?: {
1052
+ [x: string]: unknown;
1053
+ renderViaSchema?: boolean | undefined;
1054
+ hideReferenceRail?: boolean | undefined;
1055
+ hideRelatedTab?: boolean | undefined;
1056
+ } | undefined;
1033
1057
  actions?: {
1034
1058
  name: string;
1035
1059
  label: string;
@@ -3841,7 +3865,7 @@ declare const SystemUploadSession: Omit<{
3841
3865
  } | undefined;
3842
3866
  partitioning?: {
3843
3867
  enabled: boolean;
3844
- strategy: "hash" | "list" | "range";
3868
+ strategy: "list" | "hash" | "range";
3845
3869
  key: string;
3846
3870
  interval?: string | undefined;
3847
3871
  } | undefined;
@@ -4155,11 +4179,17 @@ declare const SystemUploadSession: Omit<{
4155
4179
  trash: boolean;
4156
4180
  mru: boolean;
4157
4181
  clone: boolean;
4158
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4182
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4159
4183
  } | undefined;
4160
4184
  recordTypes?: string[] | undefined;
4161
4185
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
4162
4186
  keyPrefix?: string | undefined;
4187
+ detail?: {
4188
+ [x: string]: unknown;
4189
+ renderViaSchema?: boolean | undefined;
4190
+ hideReferenceRail?: boolean | undefined;
4191
+ hideRelatedTab?: boolean | undefined;
4192
+ } | undefined;
4163
4193
  actions?: {
4164
4194
  name: string;
4165
4195
  label: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin, PluginContext } from '@objectstack/core';
2
+ import { MetricsRegistry } from '@objectstack/observability';
2
3
  import { IStorageService, StorageUploadOptions, StorageFileInfo, PresignedUploadDescriptor, PresignedDownloadDescriptor, IDataEngine, IHttpServer } from '@objectstack/spec/contracts';
3
4
  import { z } from 'zod';
4
5
  import * as _objectstack_spec_data from '@objectstack/spec/data';
@@ -28,6 +29,8 @@ interface LocalStorageAdapterOptions {
28
29
  * Auto-generated if omitted (suitable for single-process dev usage).
29
30
  */
30
31
  signingSecret?: string;
32
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
33
+ metrics?: MetricsRegistry;
31
34
  }
32
35
  interface PresignTokenPayload {
33
36
  k: string;
@@ -52,7 +55,13 @@ declare class LocalStorageAdapter implements IStorageService {
52
55
  private readonly baseUrl;
53
56
  private readonly basePath;
54
57
  private readonly signingSecret;
58
+ private readonly metrics;
55
59
  constructor(options: LocalStorageAdapterOptions);
60
+ /**
61
+ * Wrap a storage operation with metrics instrumentation. Never swallows
62
+ * the underlying error; instrumentation failures are silently ignored.
63
+ */
64
+ private track;
56
65
  private resolvePath;
57
66
  private resolvePartPath;
58
67
  upload(key: string, data: Buffer | ReadableStream, _options?: StorageUploadOptions): Promise<void>;
@@ -238,6 +247,8 @@ interface S3StorageAdapterOptions {
238
247
  secretAccessKey?: string;
239
248
  /** Force path-style URLs (needed for MinIO / self-hosted) */
240
249
  forcePathStyle?: boolean;
250
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
251
+ metrics?: MetricsRegistry;
241
252
  }
242
253
  /**
243
254
  * S3 storage adapter implementing IStorageService.
@@ -261,8 +272,15 @@ declare class S3StorageAdapter implements IStorageService {
261
272
  private readonly region;
262
273
  private readonly endpoint?;
263
274
  private readonly forcePathStyle;
275
+ private readonly metrics;
264
276
  private clientPromise;
265
277
  constructor(options: S3StorageAdapterOptions);
278
+ /**
279
+ * Wrap a storage operation with metrics instrumentation.
280
+ * Records ok/error counters, a duration histogram, and an error counter
281
+ * keyed by error class on failure. Never swallows the underlying error.
282
+ */
283
+ private track;
266
284
  /**
267
285
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
268
286
  * `@aws-sdk/client-s3` isn't installed.
@@ -711,7 +729,7 @@ declare const SystemFile: Omit<{
711
729
  } | undefined;
712
730
  partitioning?: {
713
731
  enabled: boolean;
714
- strategy: "hash" | "list" | "range";
732
+ strategy: "list" | "hash" | "range";
715
733
  key: string;
716
734
  interval?: string | undefined;
717
735
  } | undefined;
@@ -1025,11 +1043,17 @@ declare const SystemFile: Omit<{
1025
1043
  trash: boolean;
1026
1044
  mru: boolean;
1027
1045
  clone: boolean;
1028
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1046
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1029
1047
  } | undefined;
1030
1048
  recordTypes?: string[] | undefined;
1031
1049
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
1032
1050
  keyPrefix?: string | undefined;
1051
+ detail?: {
1052
+ [x: string]: unknown;
1053
+ renderViaSchema?: boolean | undefined;
1054
+ hideReferenceRail?: boolean | undefined;
1055
+ hideRelatedTab?: boolean | undefined;
1056
+ } | undefined;
1033
1057
  actions?: {
1034
1058
  name: string;
1035
1059
  label: string;
@@ -3841,7 +3865,7 @@ declare const SystemUploadSession: Omit<{
3841
3865
  } | undefined;
3842
3866
  partitioning?: {
3843
3867
  enabled: boolean;
3844
- strategy: "hash" | "list" | "range";
3868
+ strategy: "list" | "hash" | "range";
3845
3869
  key: string;
3846
3870
  interval?: string | undefined;
3847
3871
  } | undefined;
@@ -4155,11 +4179,17 @@ declare const SystemUploadSession: Omit<{
4155
4179
  trash: boolean;
4156
4180
  mru: boolean;
4157
4181
  clone: boolean;
4158
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4182
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4159
4183
  } | undefined;
4160
4184
  recordTypes?: string[] | undefined;
4161
4185
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
4162
4186
  keyPrefix?: string | undefined;
4187
+ detail?: {
4188
+ [x: string]: unknown;
4189
+ renderViaSchema?: boolean | undefined;
4190
+ hideReferenceRail?: boolean | undefined;
4191
+ hideRelatedTab?: boolean | undefined;
4192
+ } | undefined;
4163
4193
  actions?: {
4164
4194
  name: string;
4165
4195
  label: string;
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ var s3_storage_adapter_exports = {};
13
13
  __export(s3_storage_adapter_exports, {
14
14
  S3StorageAdapter: () => S3StorageAdapter
15
15
  });
16
+ import {
17
+ NoopMetricsRegistry as NoopMetricsRegistry2,
18
+ SEMCONV as SEMCONV2
19
+ } from "@objectstack/observability";
16
20
  async function streamToBuffer(stream) {
17
21
  if (Buffer.isBuffer(stream)) return stream;
18
22
  if (stream instanceof Uint8Array) return Buffer.from(stream);
@@ -50,6 +54,34 @@ var init_s3_storage_adapter = __esm({
50
54
  this.region = options.region;
51
55
  this.endpoint = options.endpoint;
52
56
  this.forcePathStyle = options.forcePathStyle ?? false;
57
+ this.metrics = options.metrics ?? new NoopMetricsRegistry2();
58
+ }
59
+ /**
60
+ * Wrap a storage operation with metrics instrumentation.
61
+ * Records ok/error counters, a duration histogram, and an error counter
62
+ * keyed by error class on failure. Never swallows the underlying error.
63
+ */
64
+ async track(op, fn) {
65
+ const started = Date.now();
66
+ const baseLabels = { adapter: "s3", op };
67
+ try {
68
+ const out = await fn();
69
+ try {
70
+ this.metrics.counter(SEMCONV2.storageOperationsTotal, { ...baseLabels, result: "ok" });
71
+ this.metrics.histogram(SEMCONV2.storageOperationDurationMs, Date.now() - started, baseLabels);
72
+ } catch {
73
+ }
74
+ return out;
75
+ } catch (err) {
76
+ try {
77
+ this.metrics.counter(SEMCONV2.storageOperationsTotal, { ...baseLabels, result: "error" });
78
+ this.metrics.histogram(SEMCONV2.storageOperationDurationMs, Date.now() - started, baseLabels);
79
+ const errorClass = err?.name || err?.constructor?.name || "Error";
80
+ this.metrics.counter(SEMCONV2.storageErrorsTotal, { ...baseLabels, errorClass });
81
+ } catch {
82
+ }
83
+ throw err;
84
+ }
53
85
  }
54
86
  /**
55
87
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
@@ -99,67 +131,79 @@ var init_s3_storage_adapter = __esm({
99
131
  // Basic operations
100
132
  // ---------------------------------------------------------------------------
101
133
  async upload(key, data, options) {
102
- const client = await this.getClient();
103
- const s3 = await this.s3Mod();
104
- const body = data instanceof Buffer ? data : await streamToBuffer(data);
105
- const cmd = new s3.PutObjectCommand({
106
- Bucket: this.bucket,
107
- Key: key,
108
- Body: body,
109
- ContentType: options?.contentType,
110
- Metadata: options?.metadata,
111
- ACL: options?.acl === "public-read" ? "public-read" : void 0
134
+ return this.track("put", async () => {
135
+ const client = await this.getClient();
136
+ const s3 = await this.s3Mod();
137
+ const body = data instanceof Buffer ? data : await streamToBuffer(data);
138
+ const cmd = new s3.PutObjectCommand({
139
+ Bucket: this.bucket,
140
+ Key: key,
141
+ Body: body,
142
+ ContentType: options?.contentType,
143
+ Metadata: options?.metadata,
144
+ ACL: options?.acl === "public-read" ? "public-read" : void 0
145
+ });
146
+ await client.send(cmd);
112
147
  });
113
- await client.send(cmd);
114
148
  }
115
149
  async download(key) {
116
- const client = await this.getClient();
117
- const s3 = await this.s3Mod();
118
- const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
119
- const res = await client.send(cmd);
120
- return streamToBuffer(res.Body);
150
+ return this.track("get", async () => {
151
+ const client = await this.getClient();
152
+ const s3 = await this.s3Mod();
153
+ const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
154
+ const res = await client.send(cmd);
155
+ return streamToBuffer(res.Body);
156
+ });
121
157
  }
122
158
  async delete(key) {
123
- const client = await this.getClient();
124
- const s3 = await this.s3Mod();
125
- const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
126
- await client.send(cmd);
159
+ return this.track("delete", async () => {
160
+ const client = await this.getClient();
161
+ const s3 = await this.s3Mod();
162
+ const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
163
+ await client.send(cmd);
164
+ });
127
165
  }
128
166
  async exists(key) {
129
- const client = await this.getClient();
130
- const s3 = await this.s3Mod();
131
- try {
132
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
133
- await client.send(cmd);
134
- return true;
135
- } catch (err) {
136
- if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
137
- throw err;
138
- }
167
+ return this.track("head", async () => {
168
+ const client = await this.getClient();
169
+ const s3 = await this.s3Mod();
170
+ try {
171
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
172
+ await client.send(cmd);
173
+ return true;
174
+ } catch (err) {
175
+ if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
176
+ throw err;
177
+ }
178
+ });
139
179
  }
140
180
  async getInfo(key) {
141
- const client = await this.getClient();
142
- const s3 = await this.s3Mod();
143
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
144
- const res = await client.send(cmd);
145
- return {
146
- key,
147
- size: res.ContentLength ?? 0,
148
- contentType: res.ContentType,
149
- lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
150
- metadata: res.Metadata
151
- };
181
+ return this.track("head", async () => {
182
+ const client = await this.getClient();
183
+ const s3 = await this.s3Mod();
184
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
185
+ const res = await client.send(cmd);
186
+ return {
187
+ key,
188
+ size: res.ContentLength ?? 0,
189
+ contentType: res.ContentType,
190
+ lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
191
+ metadata: res.Metadata
192
+ };
193
+ });
152
194
  }
153
195
  async list(prefix) {
154
- const client = await this.getClient();
155
- const s3 = await this.s3Mod();
156
- const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
157
- const res = await client.send(cmd);
158
- return (res.Contents ?? []).map((item) => ({
159
- key: item.Key,
160
- size: item.Size ?? 0,
161
- lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
162
- }));
196
+ return this.track("list", async () => {
197
+ const client = await this.getClient();
198
+ const s3 = await this.s3Mod();
199
+ const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
200
+ const res = await client.send(cmd);
201
+ return (res.Contents ?? []).map((item) => ({
202
+ key: item.Key,
203
+ size: item.Size ?? 0,
204
+ lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
205
+ }));
206
+ });
163
207
  }
164
208
  // ---------------------------------------------------------------------------
165
209
  // Presigned URLs
@@ -276,6 +320,10 @@ var init_s3_storage_adapter = __esm({
276
320
  import { promises as fs, createReadStream, createWriteStream } from "fs";
277
321
  import { join, dirname } from "path";
278
322
  import { createHmac, randomUUID } from "crypto";
323
+ import {
324
+ NoopMetricsRegistry,
325
+ SEMCONV
326
+ } from "@objectstack/observability";
279
327
  var LocalStorageAdapter = class {
280
328
  constructor(options) {
281
329
  this.rootDir = options.rootDir;
@@ -283,6 +331,33 @@ var LocalStorageAdapter = class {
283
331
  this.baseUrl = options.baseUrl ?? "";
284
332
  this.basePath = options.basePath ?? "/api/v1/storage";
285
333
  this.signingSecret = options.signingSecret ?? randomUUID();
334
+ this.metrics = options.metrics ?? new NoopMetricsRegistry();
335
+ }
336
+ /**
337
+ * Wrap a storage operation with metrics instrumentation. Never swallows
338
+ * the underlying error; instrumentation failures are silently ignored.
339
+ */
340
+ async track(op, fn) {
341
+ const started = Date.now();
342
+ const baseLabels = { adapter: "local", op };
343
+ try {
344
+ const out = await fn();
345
+ try {
346
+ this.metrics.counter(SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
347
+ this.metrics.histogram(SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
348
+ } catch {
349
+ }
350
+ return out;
351
+ } catch (err) {
352
+ try {
353
+ this.metrics.counter(SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
354
+ this.metrics.histogram(SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
355
+ const errorClass = err?.name || err?.constructor?.name || "Error";
356
+ this.metrics.counter(SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
357
+ } catch {
358
+ }
359
+ throw err;
360
+ }
286
361
  }
287
362
  // ---------------------------------------------------------------------------
288
363
  // Path helpers
@@ -303,61 +378,72 @@ var LocalStorageAdapter = class {
303
378
  // Basic file operations
304
379
  // ---------------------------------------------------------------------------
305
380
  async upload(key, data, _options) {
306
- const filePath = this.resolvePath(key);
307
- await fs.mkdir(dirname(filePath), { recursive: true });
308
- if (data instanceof Buffer) {
309
- await fs.writeFile(filePath, data);
310
- return;
311
- }
312
- const chunks = [];
313
- const reader = data.getReader();
314
- let done = false;
315
- while (!done) {
316
- const result = await reader.read();
317
- done = result.done;
318
- if (result.value) chunks.push(result.value);
319
- }
320
- await fs.writeFile(filePath, Buffer.concat(chunks));
381
+ return this.track("put", async () => {
382
+ const filePath = this.resolvePath(key);
383
+ await fs.mkdir(dirname(filePath), { recursive: true });
384
+ if (data instanceof Buffer) {
385
+ await fs.writeFile(filePath, data);
386
+ return;
387
+ }
388
+ const chunks = [];
389
+ const reader = data.getReader();
390
+ let done = false;
391
+ while (!done) {
392
+ const result = await reader.read();
393
+ done = result.done;
394
+ if (result.value) chunks.push(result.value);
395
+ }
396
+ await fs.writeFile(filePath, Buffer.concat(chunks));
397
+ });
321
398
  }
322
399
  async download(key) {
323
- return fs.readFile(this.resolvePath(key));
400
+ return this.track("get", async () => fs.readFile(this.resolvePath(key)));
324
401
  }
325
402
  async delete(key) {
326
- await fs.unlink(this.resolvePath(key)).catch((err) => {
327
- if (err && err.code === "ENOENT") return;
328
- throw err;
403
+ return this.track("delete", async () => {
404
+ await fs.unlink(this.resolvePath(key)).catch((err) => {
405
+ if (err && err.code === "ENOENT") return;
406
+ throw err;
407
+ });
329
408
  });
330
409
  }
331
410
  async exists(key) {
332
- try {
333
- await fs.access(this.resolvePath(key));
334
- return true;
335
- } catch {
336
- return false;
337
- }
411
+ return this.track("head", async () => {
412
+ try {
413
+ await fs.access(this.resolvePath(key));
414
+ return true;
415
+ } catch {
416
+ return false;
417
+ }
418
+ });
338
419
  }
339
420
  async getInfo(key) {
340
- const filePath = this.resolvePath(key);
341
- const stat = await fs.stat(filePath);
342
- return { key, size: stat.size, lastModified: stat.mtime };
421
+ return this.track("head", async () => {
422
+ const filePath = this.resolvePath(key);
423
+ const stat = await fs.stat(filePath);
424
+ return { key, size: stat.size, lastModified: stat.mtime };
425
+ });
343
426
  }
344
427
  async list(prefix) {
345
- const dirPath = this.resolvePath(prefix);
346
- try {
347
- const entries = await fs.readdir(dirPath);
348
- const results = [];
349
- for (const entry of entries) {
350
- if (entry.startsWith(".")) continue;
351
- const fullKey = prefix ? `${prefix}/${entry}` : entry;
352
- try {
353
- results.push(await this.getInfo(fullKey));
354
- } catch {
428
+ return this.track("list", async () => {
429
+ const dirPath = this.resolvePath(prefix);
430
+ try {
431
+ const entries = await fs.readdir(dirPath);
432
+ const results = [];
433
+ for (const entry of entries) {
434
+ if (entry.startsWith(".")) continue;
435
+ const fullKey = prefix ? `${prefix}/${entry}` : entry;
436
+ try {
437
+ const stat = await fs.stat(this.resolvePath(fullKey));
438
+ results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
439
+ } catch {
440
+ }
355
441
  }
442
+ return results;
443
+ } catch {
444
+ return [];
356
445
  }
357
- return results;
358
- } catch {
359
- return [];
360
- }
446
+ });
361
447
  }
362
448
  // ---------------------------------------------------------------------------
363
449
  // Presigned URL helpers