@objectstack/service-storage 5.0.0 → 5.2.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 +196 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +204 -97
- 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>;
|
|
@@ -123,6 +132,13 @@ interface StorageServicePluginOptions {
|
|
|
123
132
|
* adapter constructor-driven (useful in tests). Default: true.
|
|
124
133
|
*/
|
|
125
134
|
bindToSettings?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Optional explicit metrics backend. Wins over the service-registry
|
|
137
|
+
* lookup. Mostly an escape hatch for tests; production hosts should
|
|
138
|
+
* register `ObservabilityServicePlugin` (from `@objectstack/runtime`)
|
|
139
|
+
* once and let every service pick the host's backend up automatically.
|
|
140
|
+
*/
|
|
141
|
+
metrics?: MetricsRegistry;
|
|
126
142
|
}
|
|
127
143
|
/**
|
|
128
144
|
* StorageServicePlugin — Production IStorageService implementation.
|
|
@@ -156,6 +172,7 @@ declare class StorageServicePlugin implements Plugin {
|
|
|
156
172
|
private readonly options;
|
|
157
173
|
private storage;
|
|
158
174
|
private store;
|
|
175
|
+
private metrics;
|
|
159
176
|
constructor(options?: StorageServicePluginOptions);
|
|
160
177
|
/** Build a concrete adapter from a values map (settings-derived). */
|
|
161
178
|
private buildAdapterFromValues;
|
|
@@ -238,6 +255,8 @@ interface S3StorageAdapterOptions {
|
|
|
238
255
|
secretAccessKey?: string;
|
|
239
256
|
/** Force path-style URLs (needed for MinIO / self-hosted) */
|
|
240
257
|
forcePathStyle?: boolean;
|
|
258
|
+
/** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
|
|
259
|
+
metrics?: MetricsRegistry;
|
|
241
260
|
}
|
|
242
261
|
/**
|
|
243
262
|
* S3 storage adapter implementing IStorageService.
|
|
@@ -261,8 +280,15 @@ declare class S3StorageAdapter implements IStorageService {
|
|
|
261
280
|
private readonly region;
|
|
262
281
|
private readonly endpoint?;
|
|
263
282
|
private readonly forcePathStyle;
|
|
283
|
+
private readonly metrics;
|
|
264
284
|
private clientPromise;
|
|
265
285
|
constructor(options: S3StorageAdapterOptions);
|
|
286
|
+
/**
|
|
287
|
+
* Wrap a storage operation with metrics instrumentation.
|
|
288
|
+
* Records ok/error counters, a duration histogram, and an error counter
|
|
289
|
+
* keyed by error class on failure. Never swallows the underlying error.
|
|
290
|
+
*/
|
|
291
|
+
private track;
|
|
266
292
|
/**
|
|
267
293
|
* Lazily resolve the AWS S3 client to avoid crashing at import time when
|
|
268
294
|
* `@aws-sdk/client-s3` isn't installed.
|
|
@@ -711,7 +737,7 @@ declare const SystemFile: Omit<{
|
|
|
711
737
|
} | undefined;
|
|
712
738
|
partitioning?: {
|
|
713
739
|
enabled: boolean;
|
|
714
|
-
strategy: "
|
|
740
|
+
strategy: "list" | "hash" | "range";
|
|
715
741
|
key: string;
|
|
716
742
|
interval?: string | undefined;
|
|
717
743
|
} | undefined;
|
|
@@ -1025,7 +1051,7 @@ declare const SystemFile: Omit<{
|
|
|
1025
1051
|
trash: boolean;
|
|
1026
1052
|
mru: boolean;
|
|
1027
1053
|
clone: boolean;
|
|
1028
|
-
apiMethods?: ("get" | "
|
|
1054
|
+
apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
|
|
1029
1055
|
} | undefined;
|
|
1030
1056
|
recordTypes?: string[] | undefined;
|
|
1031
1057
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
@@ -3847,7 +3873,7 @@ declare const SystemUploadSession: Omit<{
|
|
|
3847
3873
|
} | undefined;
|
|
3848
3874
|
partitioning?: {
|
|
3849
3875
|
enabled: boolean;
|
|
3850
|
-
strategy: "
|
|
3876
|
+
strategy: "list" | "hash" | "range";
|
|
3851
3877
|
key: string;
|
|
3852
3878
|
interval?: string | undefined;
|
|
3853
3879
|
} | undefined;
|
|
@@ -4161,7 +4187,7 @@ declare const SystemUploadSession: Omit<{
|
|
|
4161
4187
|
trash: boolean;
|
|
4162
4188
|
mru: boolean;
|
|
4163
4189
|
clone: boolean;
|
|
4164
|
-
apiMethods?: ("get" | "
|
|
4190
|
+
apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
|
|
4165
4191
|
} | undefined;
|
|
4166
4192
|
recordTypes?: string[] | undefined;
|
|
4167
4193
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
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>;
|
|
@@ -123,6 +132,13 @@ interface StorageServicePluginOptions {
|
|
|
123
132
|
* adapter constructor-driven (useful in tests). Default: true.
|
|
124
133
|
*/
|
|
125
134
|
bindToSettings?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Optional explicit metrics backend. Wins over the service-registry
|
|
137
|
+
* lookup. Mostly an escape hatch for tests; production hosts should
|
|
138
|
+
* register `ObservabilityServicePlugin` (from `@objectstack/runtime`)
|
|
139
|
+
* once and let every service pick the host's backend up automatically.
|
|
140
|
+
*/
|
|
141
|
+
metrics?: MetricsRegistry;
|
|
126
142
|
}
|
|
127
143
|
/**
|
|
128
144
|
* StorageServicePlugin — Production IStorageService implementation.
|
|
@@ -156,6 +172,7 @@ declare class StorageServicePlugin implements Plugin {
|
|
|
156
172
|
private readonly options;
|
|
157
173
|
private storage;
|
|
158
174
|
private store;
|
|
175
|
+
private metrics;
|
|
159
176
|
constructor(options?: StorageServicePluginOptions);
|
|
160
177
|
/** Build a concrete adapter from a values map (settings-derived). */
|
|
161
178
|
private buildAdapterFromValues;
|
|
@@ -238,6 +255,8 @@ interface S3StorageAdapterOptions {
|
|
|
238
255
|
secretAccessKey?: string;
|
|
239
256
|
/** Force path-style URLs (needed for MinIO / self-hosted) */
|
|
240
257
|
forcePathStyle?: boolean;
|
|
258
|
+
/** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
|
|
259
|
+
metrics?: MetricsRegistry;
|
|
241
260
|
}
|
|
242
261
|
/**
|
|
243
262
|
* S3 storage adapter implementing IStorageService.
|
|
@@ -261,8 +280,15 @@ declare class S3StorageAdapter implements IStorageService {
|
|
|
261
280
|
private readonly region;
|
|
262
281
|
private readonly endpoint?;
|
|
263
282
|
private readonly forcePathStyle;
|
|
283
|
+
private readonly metrics;
|
|
264
284
|
private clientPromise;
|
|
265
285
|
constructor(options: S3StorageAdapterOptions);
|
|
286
|
+
/**
|
|
287
|
+
* Wrap a storage operation with metrics instrumentation.
|
|
288
|
+
* Records ok/error counters, a duration histogram, and an error counter
|
|
289
|
+
* keyed by error class on failure. Never swallows the underlying error.
|
|
290
|
+
*/
|
|
291
|
+
private track;
|
|
266
292
|
/**
|
|
267
293
|
* Lazily resolve the AWS S3 client to avoid crashing at import time when
|
|
268
294
|
* `@aws-sdk/client-s3` isn't installed.
|
|
@@ -711,7 +737,7 @@ declare const SystemFile: Omit<{
|
|
|
711
737
|
} | undefined;
|
|
712
738
|
partitioning?: {
|
|
713
739
|
enabled: boolean;
|
|
714
|
-
strategy: "
|
|
740
|
+
strategy: "list" | "hash" | "range";
|
|
715
741
|
key: string;
|
|
716
742
|
interval?: string | undefined;
|
|
717
743
|
} | undefined;
|
|
@@ -1025,7 +1051,7 @@ declare const SystemFile: Omit<{
|
|
|
1025
1051
|
trash: boolean;
|
|
1026
1052
|
mru: boolean;
|
|
1027
1053
|
clone: boolean;
|
|
1028
|
-
apiMethods?: ("get" | "
|
|
1054
|
+
apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
|
|
1029
1055
|
} | undefined;
|
|
1030
1056
|
recordTypes?: string[] | undefined;
|
|
1031
1057
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
@@ -3847,7 +3873,7 @@ declare const SystemUploadSession: Omit<{
|
|
|
3847
3873
|
} | undefined;
|
|
3848
3874
|
partitioning?: {
|
|
3849
3875
|
enabled: boolean;
|
|
3850
|
-
strategy: "
|
|
3876
|
+
strategy: "list" | "hash" | "range";
|
|
3851
3877
|
key: string;
|
|
3852
3878
|
interval?: string | undefined;
|
|
3853
3879
|
} | undefined;
|
|
@@ -4161,7 +4187,7 @@ declare const SystemUploadSession: Omit<{
|
|
|
4161
4187
|
trash: boolean;
|
|
4162
4188
|
mru: boolean;
|
|
4163
4189
|
clone: boolean;
|
|
4164
|
-
apiMethods?: ("get" | "
|
|
4190
|
+
apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
|
|
4165
4191
|
} | undefined;
|
|
4166
4192
|
recordTypes?: string[] | undefined;
|
|
4167
4193
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
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
|
|
@@ -272,10 +316,20 @@ var init_s3_storage_adapter = __esm({
|
|
|
272
316
|
}
|
|
273
317
|
});
|
|
274
318
|
|
|
319
|
+
// src/storage-service-plugin.ts
|
|
320
|
+
import {
|
|
321
|
+
OBSERVABILITY_METRICS_SERVICE,
|
|
322
|
+
NoopMetricsRegistry as NoopMetricsRegistry3
|
|
323
|
+
} from "@objectstack/observability";
|
|
324
|
+
|
|
275
325
|
// src/local-storage-adapter.ts
|
|
276
326
|
import { promises as fs, createReadStream, createWriteStream } from "fs";
|
|
277
327
|
import { join, dirname } from "path";
|
|
278
328
|
import { createHmac, randomUUID } from "crypto";
|
|
329
|
+
import {
|
|
330
|
+
NoopMetricsRegistry,
|
|
331
|
+
SEMCONV
|
|
332
|
+
} from "@objectstack/observability";
|
|
279
333
|
var LocalStorageAdapter = class {
|
|
280
334
|
constructor(options) {
|
|
281
335
|
this.rootDir = options.rootDir;
|
|
@@ -283,6 +337,33 @@ var LocalStorageAdapter = class {
|
|
|
283
337
|
this.baseUrl = options.baseUrl ?? "";
|
|
284
338
|
this.basePath = options.basePath ?? "/api/v1/storage";
|
|
285
339
|
this.signingSecret = options.signingSecret ?? randomUUID();
|
|
340
|
+
this.metrics = options.metrics ?? new NoopMetricsRegistry();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Wrap a storage operation with metrics instrumentation. Never swallows
|
|
344
|
+
* the underlying error; instrumentation failures are silently ignored.
|
|
345
|
+
*/
|
|
346
|
+
async track(op, fn) {
|
|
347
|
+
const started = Date.now();
|
|
348
|
+
const baseLabels = { adapter: "local", op };
|
|
349
|
+
try {
|
|
350
|
+
const out = await fn();
|
|
351
|
+
try {
|
|
352
|
+
this.metrics.counter(SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
|
|
353
|
+
this.metrics.histogram(SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
try {
|
|
359
|
+
this.metrics.counter(SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
|
|
360
|
+
this.metrics.histogram(SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
361
|
+
const errorClass = err?.name || err?.constructor?.name || "Error";
|
|
362
|
+
this.metrics.counter(SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
286
367
|
}
|
|
287
368
|
// ---------------------------------------------------------------------------
|
|
288
369
|
// Path helpers
|
|
@@ -303,61 +384,72 @@ var LocalStorageAdapter = class {
|
|
|
303
384
|
// Basic file operations
|
|
304
385
|
// ---------------------------------------------------------------------------
|
|
305
386
|
async upload(key, data, _options) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
387
|
+
return this.track("put", async () => {
|
|
388
|
+
const filePath = this.resolvePath(key);
|
|
389
|
+
await fs.mkdir(dirname(filePath), { recursive: true });
|
|
390
|
+
if (data instanceof Buffer) {
|
|
391
|
+
await fs.writeFile(filePath, data);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const chunks = [];
|
|
395
|
+
const reader = data.getReader();
|
|
396
|
+
let done = false;
|
|
397
|
+
while (!done) {
|
|
398
|
+
const result = await reader.read();
|
|
399
|
+
done = result.done;
|
|
400
|
+
if (result.value) chunks.push(result.value);
|
|
401
|
+
}
|
|
402
|
+
await fs.writeFile(filePath, Buffer.concat(chunks));
|
|
403
|
+
});
|
|
321
404
|
}
|
|
322
405
|
async download(key) {
|
|
323
|
-
return fs.readFile(this.resolvePath(key));
|
|
406
|
+
return this.track("get", async () => fs.readFile(this.resolvePath(key)));
|
|
324
407
|
}
|
|
325
408
|
async delete(key) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
409
|
+
return this.track("delete", async () => {
|
|
410
|
+
await fs.unlink(this.resolvePath(key)).catch((err) => {
|
|
411
|
+
if (err && err.code === "ENOENT") return;
|
|
412
|
+
throw err;
|
|
413
|
+
});
|
|
329
414
|
});
|
|
330
415
|
}
|
|
331
416
|
async exists(key) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
417
|
+
return this.track("head", async () => {
|
|
418
|
+
try {
|
|
419
|
+
await fs.access(this.resolvePath(key));
|
|
420
|
+
return true;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
338
425
|
}
|
|
339
426
|
async getInfo(key) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
427
|
+
return this.track("head", async () => {
|
|
428
|
+
const filePath = this.resolvePath(key);
|
|
429
|
+
const stat = await fs.stat(filePath);
|
|
430
|
+
return { key, size: stat.size, lastModified: stat.mtime };
|
|
431
|
+
});
|
|
343
432
|
}
|
|
344
433
|
async list(prefix) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
434
|
+
return this.track("list", async () => {
|
|
435
|
+
const dirPath = this.resolvePath(prefix);
|
|
436
|
+
try {
|
|
437
|
+
const entries = await fs.readdir(dirPath);
|
|
438
|
+
const results = [];
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
if (entry.startsWith(".")) continue;
|
|
441
|
+
const fullKey = prefix ? `${prefix}/${entry}` : entry;
|
|
442
|
+
try {
|
|
443
|
+
const stat = await fs.stat(this.resolvePath(fullKey));
|
|
444
|
+
results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
355
447
|
}
|
|
448
|
+
return results;
|
|
449
|
+
} catch {
|
|
450
|
+
return [];
|
|
356
451
|
}
|
|
357
|
-
|
|
358
|
-
} catch {
|
|
359
|
-
return [];
|
|
360
|
-
}
|
|
452
|
+
});
|
|
361
453
|
}
|
|
362
454
|
// ---------------------------------------------------------------------------
|
|
363
455
|
// Presigned URL helpers
|
|
@@ -1229,6 +1321,7 @@ var StorageServicePlugin = class {
|
|
|
1229
1321
|
this.type = "standard";
|
|
1230
1322
|
this.storage = null;
|
|
1231
1323
|
this.store = null;
|
|
1324
|
+
this.metrics = new NoopMetricsRegistry3();
|
|
1232
1325
|
this.options = { adapter: "local", ...options };
|
|
1233
1326
|
}
|
|
1234
1327
|
/** Build a concrete adapter from a values map (settings-derived). */
|
|
@@ -1246,7 +1339,8 @@ var StorageServicePlugin = class {
|
|
|
1246
1339
|
endpoint: values.s3_endpoint || void 0,
|
|
1247
1340
|
accessKeyId: values.s3_access_key_id || void 0,
|
|
1248
1341
|
secretAccessKey: values.s3_secret_access_key || void 0,
|
|
1249
|
-
forcePathStyle: !!values.s3_force_path_style
|
|
1342
|
+
forcePathStyle: !!values.s3_force_path_style,
|
|
1343
|
+
metrics: this.metrics
|
|
1250
1344
|
};
|
|
1251
1345
|
return new S3StorageAdapter(opts);
|
|
1252
1346
|
}
|
|
@@ -1255,10 +1349,12 @@ var StorageServicePlugin = class {
|
|
|
1255
1349
|
basePath: this.options.basePath ?? "/api/v1/storage",
|
|
1256
1350
|
...this.options.local ?? {},
|
|
1257
1351
|
// settings value wins over any constructor-provided local.rootDir
|
|
1258
|
-
rootDir
|
|
1352
|
+
rootDir,
|
|
1353
|
+
metrics: this.metrics
|
|
1259
1354
|
});
|
|
1260
1355
|
}
|
|
1261
1356
|
async init(ctx) {
|
|
1357
|
+
this.metrics = resolveMetrics(ctx, this.options.metrics);
|
|
1262
1358
|
const adapter = this.options.adapter;
|
|
1263
1359
|
let initial;
|
|
1264
1360
|
if (adapter === "s3") {
|
|
@@ -1267,11 +1363,11 @@ var StorageServicePlugin = class {
|
|
|
1267
1363
|
if (!s3Opts) {
|
|
1268
1364
|
throw new Error('StorageServicePlugin: s3 options are required when adapter is "s3"');
|
|
1269
1365
|
}
|
|
1270
|
-
initial = new S3Ctor(s3Opts);
|
|
1366
|
+
initial = new S3Ctor({ ...s3Opts, metrics: this.metrics });
|
|
1271
1367
|
} else {
|
|
1272
1368
|
const rootDir = this.options.local?.rootDir ?? "./storage";
|
|
1273
1369
|
const basePath = this.options.basePath ?? "/api/v1/storage";
|
|
1274
|
-
initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local });
|
|
1370
|
+
initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local, metrics: this.metrics });
|
|
1275
1371
|
}
|
|
1276
1372
|
this.storage = new SwappableStorageService(initial, (prev, next) => {
|
|
1277
1373
|
const prevName = prev?.constructor?.name ?? "unknown";
|
|
@@ -1281,7 +1377,9 @@ var StorageServicePlugin = class {
|
|
|
1281
1377
|
);
|
|
1282
1378
|
});
|
|
1283
1379
|
ctx.registerService("file-storage", this.storage);
|
|
1284
|
-
ctx.logger.info(
|
|
1380
|
+
ctx.logger.info(
|
|
1381
|
+
`StorageServicePlugin: registered ${adapter} storage adapter (swappable, metrics=${this.metrics.constructor?.name ?? "unknown"})`
|
|
1382
|
+
);
|
|
1285
1383
|
try {
|
|
1286
1384
|
ctx.getService("manifest").register({
|
|
1287
1385
|
id: "com.objectstack.service.storage",
|
|
@@ -1393,6 +1491,15 @@ var StorageServicePlugin = class {
|
|
|
1393
1491
|
});
|
|
1394
1492
|
}
|
|
1395
1493
|
};
|
|
1494
|
+
function resolveMetrics(ctx, override) {
|
|
1495
|
+
if (override) return override;
|
|
1496
|
+
try {
|
|
1497
|
+
const m = ctx.getService(OBSERVABILITY_METRICS_SERVICE);
|
|
1498
|
+
if (m) return m;
|
|
1499
|
+
} catch {
|
|
1500
|
+
}
|
|
1501
|
+
return new NoopMetricsRegistry3();
|
|
1502
|
+
}
|
|
1396
1503
|
|
|
1397
1504
|
// src/index.ts
|
|
1398
1505
|
init_s3_storage_adapter();
|