@objectstack/service-storage 5.0.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 +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +178 -92
- 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
|
|
@@ -312,6 +353,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
312
353
|
var import_node_fs = require("fs");
|
|
313
354
|
var import_node_path = require("path");
|
|
314
355
|
var import_node_crypto = require("crypto");
|
|
356
|
+
var import_observability = require("@objectstack/observability");
|
|
315
357
|
var LocalStorageAdapter = class {
|
|
316
358
|
constructor(options) {
|
|
317
359
|
this.rootDir = options.rootDir;
|
|
@@ -319,6 +361,33 @@ var LocalStorageAdapter = class {
|
|
|
319
361
|
this.baseUrl = options.baseUrl ?? "";
|
|
320
362
|
this.basePath = options.basePath ?? "/api/v1/storage";
|
|
321
363
|
this.signingSecret = options.signingSecret ?? (0, import_node_crypto.randomUUID)();
|
|
364
|
+
this.metrics = options.metrics ?? new import_observability.NoopMetricsRegistry();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Wrap a storage operation with metrics instrumentation. Never swallows
|
|
368
|
+
* the underlying error; instrumentation failures are silently ignored.
|
|
369
|
+
*/
|
|
370
|
+
async track(op, fn) {
|
|
371
|
+
const started = Date.now();
|
|
372
|
+
const baseLabels = { adapter: "local", op };
|
|
373
|
+
try {
|
|
374
|
+
const out = await fn();
|
|
375
|
+
try {
|
|
376
|
+
this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
|
|
377
|
+
this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
} catch (err) {
|
|
382
|
+
try {
|
|
383
|
+
this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
|
|
384
|
+
this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
|
|
385
|
+
const errorClass = err?.name || err?.constructor?.name || "Error";
|
|
386
|
+
this.metrics.counter(import_observability.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
322
391
|
}
|
|
323
392
|
// ---------------------------------------------------------------------------
|
|
324
393
|
// Path helpers
|
|
@@ -339,61 +408,72 @@ var LocalStorageAdapter = class {
|
|
|
339
408
|
// Basic file operations
|
|
340
409
|
// ---------------------------------------------------------------------------
|
|
341
410
|
async upload(key, data, _options) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
411
|
+
return this.track("put", async () => {
|
|
412
|
+
const filePath = this.resolvePath(key);
|
|
413
|
+
await import_node_fs.promises.mkdir((0, import_node_path.dirname)(filePath), { recursive: true });
|
|
414
|
+
if (data instanceof Buffer) {
|
|
415
|
+
await import_node_fs.promises.writeFile(filePath, data);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const chunks = [];
|
|
419
|
+
const reader = data.getReader();
|
|
420
|
+
let done = false;
|
|
421
|
+
while (!done) {
|
|
422
|
+
const result = await reader.read();
|
|
423
|
+
done = result.done;
|
|
424
|
+
if (result.value) chunks.push(result.value);
|
|
425
|
+
}
|
|
426
|
+
await import_node_fs.promises.writeFile(filePath, Buffer.concat(chunks));
|
|
427
|
+
});
|
|
357
428
|
}
|
|
358
429
|
async download(key) {
|
|
359
|
-
return import_node_fs.promises.readFile(this.resolvePath(key));
|
|
430
|
+
return this.track("get", async () => import_node_fs.promises.readFile(this.resolvePath(key)));
|
|
360
431
|
}
|
|
361
432
|
async delete(key) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
433
|
+
return this.track("delete", async () => {
|
|
434
|
+
await import_node_fs.promises.unlink(this.resolvePath(key)).catch((err) => {
|
|
435
|
+
if (err && err.code === "ENOENT") return;
|
|
436
|
+
throw err;
|
|
437
|
+
});
|
|
365
438
|
});
|
|
366
439
|
}
|
|
367
440
|
async exists(key) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
441
|
+
return this.track("head", async () => {
|
|
442
|
+
try {
|
|
443
|
+
await import_node_fs.promises.access(this.resolvePath(key));
|
|
444
|
+
return true;
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
374
449
|
}
|
|
375
450
|
async getInfo(key) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
451
|
+
return this.track("head", async () => {
|
|
452
|
+
const filePath = this.resolvePath(key);
|
|
453
|
+
const stat = await import_node_fs.promises.stat(filePath);
|
|
454
|
+
return { key, size: stat.size, lastModified: stat.mtime };
|
|
455
|
+
});
|
|
379
456
|
}
|
|
380
457
|
async list(prefix) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
458
|
+
return this.track("list", async () => {
|
|
459
|
+
const dirPath = this.resolvePath(prefix);
|
|
460
|
+
try {
|
|
461
|
+
const entries = await import_node_fs.promises.readdir(dirPath);
|
|
462
|
+
const results = [];
|
|
463
|
+
for (const entry of entries) {
|
|
464
|
+
if (entry.startsWith(".")) continue;
|
|
465
|
+
const fullKey = prefix ? `${prefix}/${entry}` : entry;
|
|
466
|
+
try {
|
|
467
|
+
const stat = await import_node_fs.promises.stat(this.resolvePath(fullKey));
|
|
468
|
+
results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
391
471
|
}
|
|
472
|
+
return results;
|
|
473
|
+
} catch {
|
|
474
|
+
return [];
|
|
392
475
|
}
|
|
393
|
-
|
|
394
|
-
} catch {
|
|
395
|
-
return [];
|
|
396
|
-
}
|
|
476
|
+
});
|
|
397
477
|
}
|
|
398
478
|
// ---------------------------------------------------------------------------
|
|
399
479
|
// Presigned URL helpers
|