@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.cjs +173 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +178 -92
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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: "
|
|
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" | "
|
|
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: "
|
|
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" | "
|
|
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: "
|
|
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" | "
|
|
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: "
|
|
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" | "
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
} catch {
|
|
359
|
-
return [];
|
|
360
|
-
}
|
|
446
|
+
});
|
|
361
447
|
}
|
|
362
448
|
// ---------------------------------------------------------------------------
|
|
363
449
|
// Presigned URL helpers
|