@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.cjs
CHANGED
|
@@ -56,10 +56,11 @@ async function streamToBuffer(stream) {
|
|
|
56
56
|
}
|
|
57
57
|
return Buffer.concat(chunks);
|
|
58
58
|
}
|
|
59
|
-
var S3StorageAdapter;
|
|
59
|
+
var import_observability2, S3StorageAdapter;
|
|
60
60
|
var init_s3_storage_adapter = __esm({
|
|
61
61
|
"src/s3-storage-adapter.ts"() {
|
|
62
62
|
"use strict";
|
|
63
|
+
import_observability2 = require("@objectstack/observability");
|
|
63
64
|
S3StorageAdapter = class {
|
|
64
65
|
constructor(options) {
|
|
65
66
|
this.options = options;
|
|
@@ -72,6 +73,34 @@ var init_s3_storage_adapter = __esm({
|
|
|
72
73
|
this.region = options.region;
|
|
73
74
|
this.endpoint = options.endpoint;
|
|
74
75
|
this.forcePathStyle = options.forcePathStyle ?? false;
|
|
76
|
+
this.metrics = options.metrics ?? new import_observability2.NoopMetricsRegistry();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Wrap a storage operation with metrics instrumentation.
|
|
80
|
+
* Records ok/error counters, a duration histogram, and an error counter
|
|
81
|
+
* keyed by error class on failure. Never swallows the underlying error.
|
|
82
|
+
*/
|
|
83
|
+
async track(op, fn) {
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
const baseLabels = { adapter: "s3", op };
|
|
86
|
+
try {
|
|
87
|
+
const out = await fn();
|
|
88
|
+
try {
|
|
89
|
+
this.metrics.counter(import_observability2.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
|
|
90
|
+
this.metrics.histogram(import_observability2.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
try {
|
|
96
|
+
this.metrics.counter(import_observability2.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
|
|
97
|
+
this.metrics.histogram(import_observability2.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
98
|
+
const errorClass = err?.name || err?.constructor?.name || "Error";
|
|
99
|
+
this.metrics.counter(import_observability2.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
75
104
|
}
|
|
76
105
|
/**
|
|
77
106
|
* Lazily resolve the AWS S3 client to avoid crashing at import time when
|
|
@@ -121,67 +150,79 @@ var init_s3_storage_adapter = __esm({
|
|
|
121
150
|
// Basic operations
|
|
122
151
|
// ---------------------------------------------------------------------------
|
|
123
152
|
async upload(key, data, options) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
return this.track("put", async () => {
|
|
154
|
+
const client = await this.getClient();
|
|
155
|
+
const s3 = await this.s3Mod();
|
|
156
|
+
const body = data instanceof Buffer ? data : await streamToBuffer(data);
|
|
157
|
+
const cmd = new s3.PutObjectCommand({
|
|
158
|
+
Bucket: this.bucket,
|
|
159
|
+
Key: key,
|
|
160
|
+
Body: body,
|
|
161
|
+
ContentType: options?.contentType,
|
|
162
|
+
Metadata: options?.metadata,
|
|
163
|
+
ACL: options?.acl === "public-read" ? "public-read" : void 0
|
|
164
|
+
});
|
|
165
|
+
await client.send(cmd);
|
|
134
166
|
});
|
|
135
|
-
await client.send(cmd);
|
|
136
167
|
}
|
|
137
168
|
async download(key) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
169
|
+
return this.track("get", async () => {
|
|
170
|
+
const client = await this.getClient();
|
|
171
|
+
const s3 = await this.s3Mod();
|
|
172
|
+
const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
|
|
173
|
+
const res = await client.send(cmd);
|
|
174
|
+
return streamToBuffer(res.Body);
|
|
175
|
+
});
|
|
143
176
|
}
|
|
144
177
|
async delete(key) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
return this.track("delete", async () => {
|
|
179
|
+
const client = await this.getClient();
|
|
180
|
+
const s3 = await this.s3Mod();
|
|
181
|
+
const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
|
|
182
|
+
await client.send(cmd);
|
|
183
|
+
});
|
|
149
184
|
}
|
|
150
185
|
async exists(key) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
186
|
+
return this.track("head", async () => {
|
|
187
|
+
const client = await this.getClient();
|
|
188
|
+
const s3 = await this.s3Mod();
|
|
189
|
+
try {
|
|
190
|
+
const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
|
|
191
|
+
await client.send(cmd);
|
|
192
|
+
return true;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
161
198
|
}
|
|
162
199
|
async getInfo(key) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
return this.track("head", async () => {
|
|
201
|
+
const client = await this.getClient();
|
|
202
|
+
const s3 = await this.s3Mod();
|
|
203
|
+
const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
|
|
204
|
+
const res = await client.send(cmd);
|
|
205
|
+
return {
|
|
206
|
+
key,
|
|
207
|
+
size: res.ContentLength ?? 0,
|
|
208
|
+
contentType: res.ContentType,
|
|
209
|
+
lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
|
|
210
|
+
metadata: res.Metadata
|
|
211
|
+
};
|
|
212
|
+
});
|
|
174
213
|
}
|
|
175
214
|
async list(prefix) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
return this.track("list", async () => {
|
|
216
|
+
const client = await this.getClient();
|
|
217
|
+
const s3 = await this.s3Mod();
|
|
218
|
+
const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
|
|
219
|
+
const res = await client.send(cmd);
|
|
220
|
+
return (res.Contents ?? []).map((item) => ({
|
|
221
|
+
key: item.Key,
|
|
222
|
+
size: item.Size ?? 0,
|
|
223
|
+
lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
|
|
224
|
+
}));
|
|
225
|
+
});
|
|
185
226
|
}
|
|
186
227
|
// ---------------------------------------------------------------------------
|
|
187
228
|
// Presigned URLs
|
|
@@ -308,10 +349,14 @@ __export(index_exports, {
|
|
|
308
349
|
});
|
|
309
350
|
module.exports = __toCommonJS(index_exports);
|
|
310
351
|
|
|
352
|
+
// src/storage-service-plugin.ts
|
|
353
|
+
var import_observability3 = require("@objectstack/observability");
|
|
354
|
+
|
|
311
355
|
// src/local-storage-adapter.ts
|
|
312
356
|
var import_node_fs = require("fs");
|
|
313
357
|
var import_node_path = require("path");
|
|
314
358
|
var import_node_crypto = require("crypto");
|
|
359
|
+
var import_observability = require("@objectstack/observability");
|
|
315
360
|
var LocalStorageAdapter = class {
|
|
316
361
|
constructor(options) {
|
|
317
362
|
this.rootDir = options.rootDir;
|
|
@@ -319,6 +364,33 @@ var LocalStorageAdapter = class {
|
|
|
319
364
|
this.baseUrl = options.baseUrl ?? "";
|
|
320
365
|
this.basePath = options.basePath ?? "/api/v1/storage";
|
|
321
366
|
this.signingSecret = options.signingSecret ?? (0, import_node_crypto.randomUUID)();
|
|
367
|
+
this.metrics = options.metrics ?? new import_observability.NoopMetricsRegistry();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Wrap a storage operation with metrics instrumentation. Never swallows
|
|
371
|
+
* the underlying error; instrumentation failures are silently ignored.
|
|
372
|
+
*/
|
|
373
|
+
async track(op, fn) {
|
|
374
|
+
const started = Date.now();
|
|
375
|
+
const baseLabels = { adapter: "local", op };
|
|
376
|
+
try {
|
|
377
|
+
const out = await fn();
|
|
378
|
+
try {
|
|
379
|
+
this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
|
|
380
|
+
this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
try {
|
|
386
|
+
this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
|
|
387
|
+
this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
388
|
+
const errorClass = err?.name || err?.constructor?.name || "Error";
|
|
389
|
+
this.metrics.counter(import_observability.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
322
394
|
}
|
|
323
395
|
// ---------------------------------------------------------------------------
|
|
324
396
|
// Path helpers
|
|
@@ -339,61 +411,72 @@ var LocalStorageAdapter = class {
|
|
|
339
411
|
// Basic file operations
|
|
340
412
|
// ---------------------------------------------------------------------------
|
|
341
413
|
async upload(key, data, _options) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
414
|
+
return this.track("put", async () => {
|
|
415
|
+
const filePath = this.resolvePath(key);
|
|
416
|
+
await import_node_fs.promises.mkdir((0, import_node_path.dirname)(filePath), { recursive: true });
|
|
417
|
+
if (data instanceof Buffer) {
|
|
418
|
+
await import_node_fs.promises.writeFile(filePath, data);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const chunks = [];
|
|
422
|
+
const reader = data.getReader();
|
|
423
|
+
let done = false;
|
|
424
|
+
while (!done) {
|
|
425
|
+
const result = await reader.read();
|
|
426
|
+
done = result.done;
|
|
427
|
+
if (result.value) chunks.push(result.value);
|
|
428
|
+
}
|
|
429
|
+
await import_node_fs.promises.writeFile(filePath, Buffer.concat(chunks));
|
|
430
|
+
});
|
|
357
431
|
}
|
|
358
432
|
async download(key) {
|
|
359
|
-
return import_node_fs.promises.readFile(this.resolvePath(key));
|
|
433
|
+
return this.track("get", async () => import_node_fs.promises.readFile(this.resolvePath(key)));
|
|
360
434
|
}
|
|
361
435
|
async delete(key) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
436
|
+
return this.track("delete", async () => {
|
|
437
|
+
await import_node_fs.promises.unlink(this.resolvePath(key)).catch((err) => {
|
|
438
|
+
if (err && err.code === "ENOENT") return;
|
|
439
|
+
throw err;
|
|
440
|
+
});
|
|
365
441
|
});
|
|
366
442
|
}
|
|
367
443
|
async exists(key) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
444
|
+
return this.track("head", async () => {
|
|
445
|
+
try {
|
|
446
|
+
await import_node_fs.promises.access(this.resolvePath(key));
|
|
447
|
+
return true;
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
374
452
|
}
|
|
375
453
|
async getInfo(key) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
454
|
+
return this.track("head", async () => {
|
|
455
|
+
const filePath = this.resolvePath(key);
|
|
456
|
+
const stat = await import_node_fs.promises.stat(filePath);
|
|
457
|
+
return { key, size: stat.size, lastModified: stat.mtime };
|
|
458
|
+
});
|
|
379
459
|
}
|
|
380
460
|
async list(prefix) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
461
|
+
return this.track("list", async () => {
|
|
462
|
+
const dirPath = this.resolvePath(prefix);
|
|
463
|
+
try {
|
|
464
|
+
const entries = await import_node_fs.promises.readdir(dirPath);
|
|
465
|
+
const results = [];
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
if (entry.startsWith(".")) continue;
|
|
468
|
+
const fullKey = prefix ? `${prefix}/${entry}` : entry;
|
|
469
|
+
try {
|
|
470
|
+
const stat = await import_node_fs.promises.stat(this.resolvePath(fullKey));
|
|
471
|
+
results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
391
474
|
}
|
|
475
|
+
return results;
|
|
476
|
+
} catch {
|
|
477
|
+
return [];
|
|
392
478
|
}
|
|
393
|
-
|
|
394
|
-
} catch {
|
|
395
|
-
return [];
|
|
396
|
-
}
|
|
479
|
+
});
|
|
397
480
|
}
|
|
398
481
|
// ---------------------------------------------------------------------------
|
|
399
482
|
// Presigned URL helpers
|
|
@@ -1265,6 +1348,7 @@ var StorageServicePlugin = class {
|
|
|
1265
1348
|
this.type = "standard";
|
|
1266
1349
|
this.storage = null;
|
|
1267
1350
|
this.store = null;
|
|
1351
|
+
this.metrics = new import_observability3.NoopMetricsRegistry();
|
|
1268
1352
|
this.options = { adapter: "local", ...options };
|
|
1269
1353
|
}
|
|
1270
1354
|
/** Build a concrete adapter from a values map (settings-derived). */
|
|
@@ -1282,7 +1366,8 @@ var StorageServicePlugin = class {
|
|
|
1282
1366
|
endpoint: values.s3_endpoint || void 0,
|
|
1283
1367
|
accessKeyId: values.s3_access_key_id || void 0,
|
|
1284
1368
|
secretAccessKey: values.s3_secret_access_key || void 0,
|
|
1285
|
-
forcePathStyle: !!values.s3_force_path_style
|
|
1369
|
+
forcePathStyle: !!values.s3_force_path_style,
|
|
1370
|
+
metrics: this.metrics
|
|
1286
1371
|
};
|
|
1287
1372
|
return new S3StorageAdapter(opts);
|
|
1288
1373
|
}
|
|
@@ -1291,10 +1376,12 @@ var StorageServicePlugin = class {
|
|
|
1291
1376
|
basePath: this.options.basePath ?? "/api/v1/storage",
|
|
1292
1377
|
...this.options.local ?? {},
|
|
1293
1378
|
// settings value wins over any constructor-provided local.rootDir
|
|
1294
|
-
rootDir
|
|
1379
|
+
rootDir,
|
|
1380
|
+
metrics: this.metrics
|
|
1295
1381
|
});
|
|
1296
1382
|
}
|
|
1297
1383
|
async init(ctx) {
|
|
1384
|
+
this.metrics = resolveMetrics(ctx, this.options.metrics);
|
|
1298
1385
|
const adapter = this.options.adapter;
|
|
1299
1386
|
let initial;
|
|
1300
1387
|
if (adapter === "s3") {
|
|
@@ -1303,11 +1390,11 @@ var StorageServicePlugin = class {
|
|
|
1303
1390
|
if (!s3Opts) {
|
|
1304
1391
|
throw new Error('StorageServicePlugin: s3 options are required when adapter is "s3"');
|
|
1305
1392
|
}
|
|
1306
|
-
initial = new S3Ctor(s3Opts);
|
|
1393
|
+
initial = new S3Ctor({ ...s3Opts, metrics: this.metrics });
|
|
1307
1394
|
} else {
|
|
1308
1395
|
const rootDir = this.options.local?.rootDir ?? "./storage";
|
|
1309
1396
|
const basePath = this.options.basePath ?? "/api/v1/storage";
|
|
1310
|
-
initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local });
|
|
1397
|
+
initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local, metrics: this.metrics });
|
|
1311
1398
|
}
|
|
1312
1399
|
this.storage = new SwappableStorageService(initial, (prev, next) => {
|
|
1313
1400
|
const prevName = prev?.constructor?.name ?? "unknown";
|
|
@@ -1317,7 +1404,9 @@ var StorageServicePlugin = class {
|
|
|
1317
1404
|
);
|
|
1318
1405
|
});
|
|
1319
1406
|
ctx.registerService("file-storage", this.storage);
|
|
1320
|
-
ctx.logger.info(
|
|
1407
|
+
ctx.logger.info(
|
|
1408
|
+
`StorageServicePlugin: registered ${adapter} storage adapter (swappable, metrics=${this.metrics.constructor?.name ?? "unknown"})`
|
|
1409
|
+
);
|
|
1321
1410
|
try {
|
|
1322
1411
|
ctx.getService("manifest").register({
|
|
1323
1412
|
id: "com.objectstack.service.storage",
|
|
@@ -1429,6 +1518,15 @@ var StorageServicePlugin = class {
|
|
|
1429
1518
|
});
|
|
1430
1519
|
}
|
|
1431
1520
|
};
|
|
1521
|
+
function resolveMetrics(ctx, override) {
|
|
1522
|
+
if (override) return override;
|
|
1523
|
+
try {
|
|
1524
|
+
const m = ctx.getService(import_observability3.OBSERVABILITY_METRICS_SERVICE);
|
|
1525
|
+
if (m) return m;
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
return new import_observability3.NoopMetricsRegistry();
|
|
1529
|
+
}
|
|
1432
1530
|
|
|
1433
1531
|
// src/index.ts
|
|
1434
1532
|
init_s3_storage_adapter();
|