@h-ai/storage 0.1.0-alpha5

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/node.js ADDED
@@ -0,0 +1,959 @@
1
+ import { HaiStorageError, StorageConfigSchema } from './chunk-KHZOR6CR.js';
2
+ export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageTypeSchema } from './chunk-KHZOR6CR.js';
3
+ import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-U427BQIQ.js';
4
+ export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-U427BQIQ.js';
5
+ import { core, err, ok } from '@h-ai/core';
6
+ import { Buffer } from 'buffer';
7
+ import * as crypto from 'crypto';
8
+ import * as fs from 'fs';
9
+ import * as fsp from 'fs/promises';
10
+ import * as path from 'path';
11
+ import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, CopyObjectCommand, DeleteObjectsCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
12
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
13
+
14
+ var logger = core.logger.child({ module: "storage", scope: "provider-local" });
15
+ function toStorageError(error, key) {
16
+ const e = error;
17
+ if (e.code === "ENOENT") {
18
+ return {
19
+ ...HaiStorageError.NOT_FOUND,
20
+ message: storageM("storage_fileNotFound", { params: { key: key ?? "" } }),
21
+ cause: error
22
+ };
23
+ }
24
+ if (e.code === "EACCES" || e.code === "EPERM") {
25
+ return {
26
+ ...HaiStorageError.PERMISSION_DENIED,
27
+ message: storageM("storage_permissionDenied", { params: { key: key ?? "" } }),
28
+ cause: error
29
+ };
30
+ }
31
+ if (e.code === "ENOSPC") {
32
+ return {
33
+ ...HaiStorageError.QUOTA_EXCEEDED,
34
+ message: storageM("storage_diskSpaceInsufficient"),
35
+ cause: error
36
+ };
37
+ }
38
+ if (e.code === "EISDIR") {
39
+ return {
40
+ ...HaiStorageError.INVALID_PATH,
41
+ message: storageM("storage_pathIsDir", { params: { key: key ?? "" } }),
42
+ cause: error
43
+ };
44
+ }
45
+ if (e.code === "EINVAL") {
46
+ return {
47
+ ...HaiStorageError.INVALID_PATH,
48
+ message: e.message || storageM("storage_pathTraversal"),
49
+ cause: error
50
+ };
51
+ }
52
+ return {
53
+ code: HaiStorageError.IO_ERROR.code,
54
+ message: storageM("storage_ioError", { params: { error: e.message ?? "" } }),
55
+ cause: error
56
+ };
57
+ }
58
+ function getMimeType2(filePath) {
59
+ const ext = path.extname(filePath).toLowerCase().replace(".", "");
60
+ return MIME_TYPES[ext] || MIME_TYPE_DEFAULT;
61
+ }
62
+ function calculateEtag(stat2) {
63
+ return `"${stat2.size.toString(16)}-${stat2.mtimeMs.toString(16)}"`;
64
+ }
65
+ function isMetaFile(key) {
66
+ return key.endsWith(".meta.json");
67
+ }
68
+ function createInvalidPathError() {
69
+ const error = new Error(storageM("storage_pathTraversal"));
70
+ error.code = "EINVAL";
71
+ return error;
72
+ }
73
+ function hasPathTraversalSegment(key) {
74
+ const segments = key.split(/[\\/]+/).filter(Boolean);
75
+ return segments.includes("..");
76
+ }
77
+ function safePath(root, key) {
78
+ if (path.isAbsolute(key)) {
79
+ throw createInvalidPathError();
80
+ }
81
+ if (key.includes("\0") || hasPathTraversalSegment(key)) {
82
+ throw createInvalidPathError();
83
+ }
84
+ const normalized = path.normalize(key);
85
+ const fullPath = path.resolve(root, normalized);
86
+ const realRoot = path.resolve(root);
87
+ const inRoot = fullPath === realRoot || fullPath.startsWith(`${realRoot}${path.sep}`);
88
+ if (!inRoot) {
89
+ throw createInvalidPathError();
90
+ }
91
+ return fullPath;
92
+ }
93
+ function createLocalProvider() {
94
+ let config = null;
95
+ let connected = false;
96
+ function getConfig() {
97
+ if (!config) {
98
+ throw new Error(storageM("storage_localNotInitialized"));
99
+ }
100
+ return config;
101
+ }
102
+ function fullPath(key) {
103
+ return safePath(getConfig().root, key);
104
+ }
105
+ function toBuffer(data) {
106
+ if (typeof data === "string") {
107
+ return Buffer.from(data);
108
+ }
109
+ if (data instanceof Buffer) {
110
+ return data;
111
+ }
112
+ return Buffer.from(data);
113
+ }
114
+ const file = {
115
+ async put(key, data, options = {}) {
116
+ if (isMetaFile(key)) {
117
+ return err(
118
+ HaiStorageError.INVALID_PATH,
119
+ storageM("storage_metaFileAccess", { params: { key } })
120
+ );
121
+ }
122
+ try {
123
+ const filePath = fullPath(key);
124
+ const buffer = toBuffer(data);
125
+ logger.debug("Putting file", { key, size: buffer.length });
126
+ const dir2 = path.dirname(filePath);
127
+ await fsp.mkdir(dir2, { recursive: true, mode: getConfig().directoryMode });
128
+ await fsp.writeFile(filePath, buffer, { mode: getConfig().fileMode });
129
+ const stat2 = await fsp.stat(filePath);
130
+ const metadata = {
131
+ key,
132
+ size: stat2.size,
133
+ contentType: options.contentType || getMimeType2(key),
134
+ lastModified: stat2.mtime,
135
+ etag: calculateEtag(stat2),
136
+ metadata: options.metadata
137
+ };
138
+ if (options.metadata) {
139
+ const metaPath = `${filePath}.meta.json`;
140
+ await fsp.writeFile(metaPath, JSON.stringify({
141
+ contentType: options.contentType,
142
+ metadata: options.metadata
143
+ }));
144
+ }
145
+ return ok(metadata);
146
+ } catch (error) {
147
+ logger.warn("Put file failed", { key, error });
148
+ return err(toStorageError(error, key));
149
+ }
150
+ },
151
+ async get(key, options = {}) {
152
+ if (isMetaFile(key)) {
153
+ return err(
154
+ HaiStorageError.INVALID_PATH,
155
+ storageM("storage_metaFileAccess", { params: { key } })
156
+ );
157
+ }
158
+ try {
159
+ const filePath = fullPath(key);
160
+ if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
161
+ const stat2 = await fsp.stat(filePath);
162
+ const start = options.rangeStart ?? 0;
163
+ const end = options.rangeEnd !== void 0 ? options.rangeEnd : stat2.size - 1;
164
+ return new Promise((resolve2) => {
165
+ const chunks = [];
166
+ const stream = fs.createReadStream(filePath, { start, end });
167
+ stream.on("data", (chunk) => chunks.push(chunk));
168
+ stream.on("end", () => resolve2(ok(Buffer.concat(chunks))));
169
+ stream.on("error", (error) => resolve2(err(toStorageError(error, key))));
170
+ });
171
+ }
172
+ const data = await fsp.readFile(filePath);
173
+ return ok(data);
174
+ } catch (error) {
175
+ return err(toStorageError(error, key));
176
+ }
177
+ },
178
+ async head(key) {
179
+ if (isMetaFile(key)) {
180
+ return err(
181
+ HaiStorageError.INVALID_PATH,
182
+ storageM("storage_metaFileAccess", { params: { key } })
183
+ );
184
+ }
185
+ try {
186
+ const filePath = fullPath(key);
187
+ const stat2 = await fsp.stat(filePath);
188
+ if (stat2.isDirectory()) {
189
+ return err(
190
+ HaiStorageError.INVALID_PATH,
191
+ storageM("storage_pathIsDir", { params: { key } })
192
+ );
193
+ }
194
+ let customMetadata = {};
195
+ try {
196
+ const metaPath = `${filePath}.meta.json`;
197
+ const metaContent = await fsp.readFile(metaPath, "utf-8");
198
+ customMetadata = JSON.parse(metaContent);
199
+ } catch {
200
+ }
201
+ const etag = `"${stat2.size.toString(16)}-${stat2.mtimeMs.toString(16)}"`;
202
+ return ok({
203
+ key,
204
+ size: stat2.size,
205
+ contentType: customMetadata.contentType || getMimeType2(key),
206
+ lastModified: stat2.mtime,
207
+ etag,
208
+ metadata: customMetadata.metadata
209
+ });
210
+ } catch (error) {
211
+ return err(toStorageError(error, key));
212
+ }
213
+ },
214
+ async exists(key) {
215
+ if (isMetaFile(key)) {
216
+ return err(
217
+ HaiStorageError.INVALID_PATH,
218
+ storageM("storage_metaFileAccess", { params: { key } })
219
+ );
220
+ }
221
+ try {
222
+ const filePath = fullPath(key);
223
+ await fsp.access(filePath, fs.constants.F_OK);
224
+ return ok(true);
225
+ } catch (error) {
226
+ if (error.code === "ENOENT") {
227
+ return ok(false);
228
+ }
229
+ return err(toStorageError(error, key));
230
+ }
231
+ },
232
+ async delete(key) {
233
+ if (isMetaFile(key)) {
234
+ return err(
235
+ HaiStorageError.INVALID_PATH,
236
+ storageM("storage_metaFileAccess", { params: { key } })
237
+ );
238
+ }
239
+ try {
240
+ const filePath = fullPath(key);
241
+ logger.debug("Deleting file", { key });
242
+ await fsp.unlink(filePath);
243
+ try {
244
+ await fsp.unlink(`${filePath}.meta.json`);
245
+ } catch {
246
+ }
247
+ return ok(void 0);
248
+ } catch (error) {
249
+ if (error.code === "ENOENT") {
250
+ return ok(void 0);
251
+ }
252
+ logger.warn("Delete file failed", { key, error });
253
+ return err(toStorageError(error, key));
254
+ }
255
+ },
256
+ async deleteMany(keys) {
257
+ const results = await Promise.allSettled(keys.map((key) => file.delete(key)));
258
+ const errors = [];
259
+ for (const result of results) {
260
+ if (result.status === "fulfilled" && !result.value.success) {
261
+ errors.push(result.value.error);
262
+ }
263
+ }
264
+ if (errors.length > 0) {
265
+ return err(
266
+ HaiStorageError.OPERATION_FAILED,
267
+ storageM("storage_deleteManyFailed", { params: { count: errors.length } }),
268
+ errors
269
+ );
270
+ }
271
+ return ok(void 0);
272
+ },
273
+ async copy(sourceKey, destKey, options = {}) {
274
+ if (isMetaFile(sourceKey) || isMetaFile(destKey)) {
275
+ return err(
276
+ HaiStorageError.INVALID_PATH,
277
+ storageM("storage_metaFileAccess", { params: { key: sourceKey } }),
278
+ sourceKey
279
+ );
280
+ }
281
+ try {
282
+ const sourcePath = fullPath(sourceKey);
283
+ const destPath = fullPath(destKey);
284
+ logger.debug("Copying file", { sourceKey, destKey });
285
+ await fsp.mkdir(path.dirname(destPath), { recursive: true, mode: getConfig().directoryMode });
286
+ await fsp.copyFile(sourcePath, destPath);
287
+ const stat2 = await fsp.stat(destPath);
288
+ const metadata = {
289
+ key: destKey,
290
+ size: stat2.size,
291
+ contentType: options.contentType || getMimeType2(destKey),
292
+ lastModified: stat2.mtime,
293
+ etag: calculateEtag(stat2),
294
+ metadata: options.metadata
295
+ };
296
+ if (options.contentType || options.metadata) {
297
+ const metaPath = `${destPath}.meta.json`;
298
+ await fsp.writeFile(metaPath, JSON.stringify({
299
+ contentType: options.contentType,
300
+ metadata: options.metadata
301
+ }));
302
+ }
303
+ return ok(metadata);
304
+ } catch (error) {
305
+ return err(toStorageError(error, sourceKey));
306
+ }
307
+ }
308
+ };
309
+ const dir = {
310
+ async list(options = {}) {
311
+ try {
312
+ const cfg = getConfig();
313
+ const prefix = options.prefix || "";
314
+ const delimiter = options.delimiter || "";
315
+ const maxKeys = options.maxKeys || 1e3;
316
+ const basePath = safePath(cfg.root, prefix);
317
+ const files = [];
318
+ const commonPrefixes = /* @__PURE__ */ new Set();
319
+ async function readDir(dirPath) {
320
+ let entries;
321
+ try {
322
+ entries = await fsp.readdir(dirPath, { withFileTypes: true });
323
+ } catch {
324
+ return;
325
+ }
326
+ for (const entry of entries) {
327
+ if (files.length >= maxKeys)
328
+ return;
329
+ const fullEntryPath = path.join(dirPath, entry.name);
330
+ const key = path.relative(cfg.root, fullEntryPath).replace(/\\/g, "/");
331
+ if (entry.name.endsWith(".meta.json"))
332
+ continue;
333
+ if (entry.isDirectory()) {
334
+ if (delimiter) {
335
+ commonPrefixes.add(`${key}/`);
336
+ } else {
337
+ await readDir(fullEntryPath);
338
+ }
339
+ } else if (entry.isFile()) {
340
+ const stat2 = await fsp.stat(fullEntryPath);
341
+ files.push({
342
+ key,
343
+ size: stat2.size,
344
+ contentType: getMimeType2(key),
345
+ lastModified: stat2.mtime
346
+ });
347
+ }
348
+ }
349
+ }
350
+ await readDir(basePath);
351
+ return ok({
352
+ files,
353
+ commonPrefixes: Array.from(commonPrefixes),
354
+ isTruncated: files.length >= maxKeys
355
+ });
356
+ } catch (error) {
357
+ return err(toStorageError(error));
358
+ }
359
+ },
360
+ async delete(prefix) {
361
+ try {
362
+ const dirPath = fullPath(prefix);
363
+ await fsp.rm(dirPath, { recursive: true, force: true });
364
+ return ok(void 0);
365
+ } catch (error) {
366
+ if (error.code === "ENOENT") {
367
+ return ok(void 0);
368
+ }
369
+ return err(toStorageError(error, prefix));
370
+ }
371
+ }
372
+ };
373
+ const presign = {
374
+ async getUrl(key, options) {
375
+ const expiresIn = options?.expiresIn || 3600;
376
+ const expires = Math.floor(Date.now() / 1e3) + expiresIn;
377
+ const signature = crypto.createHash("sha256").update(`${key}:${expires}`).digest("hex").slice(0, 16);
378
+ return ok(`local://${key}?expires=${expires}&signature=${signature}`);
379
+ },
380
+ async putUrl(key, options) {
381
+ const expiresIn = options?.expiresIn || 3600;
382
+ const expires = Math.floor(Date.now() / 1e3) + expiresIn;
383
+ const signature = crypto.createHash("sha256").update(`put:${key}:${expires}`).digest("hex").slice(0, 16);
384
+ return ok(`local://${key}?action=put&expires=${expires}&signature=${signature}`);
385
+ },
386
+ publicUrl(_key) {
387
+ return null;
388
+ }
389
+ };
390
+ return {
391
+ name: "local",
392
+ file,
393
+ dir,
394
+ presign,
395
+ async connect(cfg) {
396
+ if (cfg.type !== "local") {
397
+ return err(
398
+ HaiStorageError.CONFIG_ERROR,
399
+ storageM("storage_localConfigTypeError")
400
+ );
401
+ }
402
+ try {
403
+ config = cfg;
404
+ logger.info("Connecting local provider", { root: cfg.root });
405
+ await fsp.mkdir(cfg.root, { recursive: true, mode: cfg.directoryMode });
406
+ connected = true;
407
+ logger.info("Local provider connected", { root: cfg.root });
408
+ return ok(void 0);
409
+ } catch (error) {
410
+ logger.error("Local provider connect failed", { error });
411
+ config = null;
412
+ return err(toStorageError(error));
413
+ }
414
+ },
415
+ async close() {
416
+ logger.info("Local provider disconnected");
417
+ config = null;
418
+ connected = false;
419
+ },
420
+ isConnected() {
421
+ return connected;
422
+ }
423
+ };
424
+ }
425
+ var logger2 = core.logger.child({ module: "storage", scope: "provider-s3" });
426
+ function sanitizeEndpointUrl(url) {
427
+ try {
428
+ const parsed = new URL(url);
429
+ if (parsed.username)
430
+ parsed.username = "***";
431
+ if (parsed.password)
432
+ parsed.password = "***";
433
+ return parsed.toString();
434
+ } catch {
435
+ return "(invalid url)";
436
+ }
437
+ }
438
+ function toStorageError2(error, key) {
439
+ const e = error;
440
+ if (e.name === "NoSuchKey" || e.Code === "NoSuchKey" || e.name === "NotFound" || e.Code === "NotFound" || e.name === "404" || e.$metadata?.httpStatusCode === 404) {
441
+ return {
442
+ ...HaiStorageError.NOT_FOUND,
443
+ message: storageM("storage_fileNotFound", { params: { key: key ?? "" } }),
444
+ cause: error
445
+ };
446
+ }
447
+ if (e.name === "AccessDenied" || e.Code === "AccessDenied") {
448
+ return {
449
+ ...HaiStorageError.PERMISSION_DENIED,
450
+ message: storageM("storage_permissionDenied", { params: { key: key ?? "" } }),
451
+ cause: error
452
+ };
453
+ }
454
+ if (e.name === "NoSuchBucket" || e.Code === "NoSuchBucket") {
455
+ return {
456
+ ...HaiStorageError.CONFIG_ERROR,
457
+ message: storageM("storage_bucketNotExist"),
458
+ cause: error
459
+ };
460
+ }
461
+ if (e.name === "NetworkError" || e.name?.includes("ECONNREFUSED")) {
462
+ return {
463
+ ...HaiStorageError.NETWORK_ERROR,
464
+ message: storageM("storage_networkError"),
465
+ cause: error
466
+ };
467
+ }
468
+ return {
469
+ ...HaiStorageError.OPERATION_FAILED,
470
+ message: storageM("storage_operationFailed", { params: { error: e.message ?? "" } }),
471
+ cause: error
472
+ };
473
+ }
474
+ function withPrefix(key, prefix) {
475
+ if (!prefix)
476
+ return key;
477
+ return `${prefix.replace(/\/+$/, "")}/${key.replace(/^\/+/, "")}`;
478
+ }
479
+ function withoutPrefix(key, prefix) {
480
+ if (!prefix)
481
+ return key;
482
+ const normalizedPrefix = `${prefix.replace(/\/+$/, "")}/`;
483
+ if (key.startsWith(normalizedPrefix)) {
484
+ return key.slice(normalizedPrefix.length);
485
+ }
486
+ return key;
487
+ }
488
+ function createS3Provider() {
489
+ let client = null;
490
+ let config = null;
491
+ function getClient() {
492
+ if (!client) {
493
+ throw new Error(storageM("storage_s3ClientNotInitialized"));
494
+ }
495
+ return client;
496
+ }
497
+ function getConfig() {
498
+ if (!config) {
499
+ throw new Error(storageM("storage_s3ConfigNotInitialized"));
500
+ }
501
+ return config;
502
+ }
503
+ function fullKey(key) {
504
+ return withPrefix(key, getConfig().prefix);
505
+ }
506
+ const file = {
507
+ async put(key, data, options = {}) {
508
+ try {
509
+ const s3Client = getClient();
510
+ const s3Config = getConfig();
511
+ const fullPath = fullKey(key);
512
+ const body = typeof data === "string" ? Buffer.from(data) : data;
513
+ await s3Client.send(new PutObjectCommand({
514
+ Bucket: s3Config.bucket,
515
+ Key: fullPath,
516
+ Body: body,
517
+ ContentType: options.contentType,
518
+ Metadata: options.metadata,
519
+ CacheControl: options.cacheControl,
520
+ ContentDisposition: options.contentDisposition
521
+ }));
522
+ const headResult = await file.head(key);
523
+ if (headResult.success) {
524
+ return headResult;
525
+ }
526
+ return ok({
527
+ key,
528
+ size: body.length,
529
+ contentType: options.contentType || "application/octet-stream",
530
+ lastModified: /* @__PURE__ */ new Date()
531
+ });
532
+ } catch (error) {
533
+ return err(toStorageError2(error, key));
534
+ }
535
+ },
536
+ async get(key, options = {}) {
537
+ try {
538
+ const s3Client = getClient();
539
+ const s3Config = getConfig();
540
+ const fullPath = fullKey(key);
541
+ let Range;
542
+ if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
543
+ const start = options.rangeStart ?? 0;
544
+ const end = options.rangeEnd !== void 0 ? options.rangeEnd : "";
545
+ Range = `bytes=${start}-${end}`;
546
+ }
547
+ const response = await s3Client.send(new GetObjectCommand({
548
+ Bucket: s3Config.bucket,
549
+ Key: fullPath,
550
+ Range
551
+ }));
552
+ if (!response.Body) {
553
+ return err(
554
+ HaiStorageError.OPERATION_FAILED,
555
+ storageM("storage_responseBodyEmpty")
556
+ );
557
+ }
558
+ const chunks = [];
559
+ const stream = response.Body;
560
+ for await (const chunk of stream) {
561
+ chunks.push(chunk);
562
+ }
563
+ return ok(Buffer.concat(chunks));
564
+ } catch (error) {
565
+ return err(toStorageError2(error, key));
566
+ }
567
+ },
568
+ async head(key) {
569
+ try {
570
+ const s3Client = getClient();
571
+ const s3Config = getConfig();
572
+ const fullPath = fullKey(key);
573
+ const response = await s3Client.send(new HeadObjectCommand({
574
+ Bucket: s3Config.bucket,
575
+ Key: fullPath
576
+ }));
577
+ return ok({
578
+ key,
579
+ size: response.ContentLength || 0,
580
+ contentType: response.ContentType || "application/octet-stream",
581
+ lastModified: response.LastModified || /* @__PURE__ */ new Date(),
582
+ etag: response.ETag,
583
+ metadata: response.Metadata
584
+ });
585
+ } catch (error) {
586
+ return err(toStorageError2(error, key));
587
+ }
588
+ },
589
+ async exists(key) {
590
+ const result = await file.head(key);
591
+ if (result.success) {
592
+ return ok(true);
593
+ }
594
+ if (result.error.code === HaiStorageError.NOT_FOUND.code) {
595
+ return ok(false);
596
+ }
597
+ return err(result.error);
598
+ },
599
+ async delete(key) {
600
+ try {
601
+ const s3Client = getClient();
602
+ const s3Config = getConfig();
603
+ const fullPath = fullKey(key);
604
+ await s3Client.send(new DeleteObjectCommand({
605
+ Bucket: s3Config.bucket,
606
+ Key: fullPath
607
+ }));
608
+ return ok(void 0);
609
+ } catch (error) {
610
+ return err(toStorageError2(error, key));
611
+ }
612
+ },
613
+ async deleteMany(keys) {
614
+ try {
615
+ const s3Client = getClient();
616
+ const s3Config = getConfig();
617
+ if (keys.length === 0) {
618
+ return ok(void 0);
619
+ }
620
+ await s3Client.send(new DeleteObjectsCommand({
621
+ Bucket: s3Config.bucket,
622
+ Delete: {
623
+ Objects: keys.map((key) => ({ Key: fullKey(key) }))
624
+ }
625
+ }));
626
+ return ok(void 0);
627
+ } catch (error) {
628
+ return err(toStorageError2(error));
629
+ }
630
+ },
631
+ async copy(sourceKey, destKey, options = {}) {
632
+ try {
633
+ const s3Client = getClient();
634
+ const s3Config = getConfig();
635
+ const sourceFullPath = fullKey(sourceKey);
636
+ const destFullPath = fullKey(destKey);
637
+ await s3Client.send(new CopyObjectCommand({
638
+ Bucket: s3Config.bucket,
639
+ CopySource: `${s3Config.bucket}/${sourceFullPath}`,
640
+ Key: destFullPath,
641
+ ContentType: options.contentType,
642
+ Metadata: options.metadata,
643
+ MetadataDirective: options.metadata || options.contentType ? "REPLACE" : "COPY"
644
+ }));
645
+ return file.head(destKey);
646
+ } catch (error) {
647
+ return err(toStorageError2(error, sourceKey));
648
+ }
649
+ }
650
+ };
651
+ const dir = {
652
+ async list(options = {}) {
653
+ try {
654
+ const s3Client = getClient();
655
+ const s3Config = getConfig();
656
+ let prefix = s3Config.prefix || "";
657
+ if (options.prefix) {
658
+ prefix = withPrefix(options.prefix, s3Config.prefix);
659
+ }
660
+ const response = await s3Client.send(new ListObjectsV2Command({
661
+ Bucket: s3Config.bucket,
662
+ Prefix: prefix || void 0,
663
+ Delimiter: options.delimiter,
664
+ MaxKeys: options.maxKeys,
665
+ ContinuationToken: options.continuationToken
666
+ }));
667
+ const files = (response.Contents || []).map((item) => ({
668
+ key: withoutPrefix(item.Key || "", s3Config.prefix),
669
+ size: item.Size || 0,
670
+ contentType: "application/octet-stream",
671
+ // ListObjects 不返回 ContentType
672
+ lastModified: item.LastModified || /* @__PURE__ */ new Date(),
673
+ etag: item.ETag
674
+ }));
675
+ const commonPrefixes = (response.CommonPrefixes || []).map((p) => withoutPrefix(p.Prefix || "", s3Config.prefix)).filter((p) => p.length > 0);
676
+ return ok({
677
+ files,
678
+ commonPrefixes,
679
+ nextContinuationToken: response.NextContinuationToken,
680
+ isTruncated: response.IsTruncated || false
681
+ });
682
+ } catch (error) {
683
+ return err(toStorageError2(error));
684
+ }
685
+ },
686
+ async delete(prefix) {
687
+ try {
688
+ let continuationToken;
689
+ do {
690
+ const listResult = await dir.list({
691
+ prefix,
692
+ continuationToken,
693
+ maxKeys: 1e3
694
+ });
695
+ if (!listResult.success) {
696
+ return err(listResult.error);
697
+ }
698
+ const keys = listResult.data.files.map((f) => f.key);
699
+ if (keys.length > 0) {
700
+ const deleteResult = await file.deleteMany(keys);
701
+ if (!deleteResult.success) {
702
+ return deleteResult;
703
+ }
704
+ }
705
+ continuationToken = listResult.data.nextContinuationToken;
706
+ } while (continuationToken);
707
+ return ok(void 0);
708
+ } catch (error) {
709
+ return err(toStorageError2(error));
710
+ }
711
+ }
712
+ };
713
+ const presign = {
714
+ async getUrl(key, options) {
715
+ try {
716
+ const s3Client = getClient();
717
+ const s3Config = getConfig();
718
+ const fullPath = fullKey(key);
719
+ const command = new GetObjectCommand({
720
+ Bucket: s3Config.bucket,
721
+ Key: fullPath,
722
+ ResponseContentType: options?.responseContentType,
723
+ ResponseContentDisposition: options?.responseContentDisposition
724
+ });
725
+ const url = await getSignedUrl(s3Client, command, {
726
+ expiresIn: options?.expiresIn || 3600
727
+ });
728
+ return ok(url);
729
+ } catch (error) {
730
+ return err(
731
+ HaiStorageError.PRESIGN_FAILED,
732
+ storageM("storage_presignUrlFailed"),
733
+ error
734
+ );
735
+ }
736
+ },
737
+ async putUrl(key, options) {
738
+ try {
739
+ const s3Client = getClient();
740
+ const s3Config = getConfig();
741
+ const fullPath = fullKey(key);
742
+ const command = new PutObjectCommand({
743
+ Bucket: s3Config.bucket,
744
+ Key: fullPath,
745
+ ContentType: options?.contentType || "application/octet-stream"
746
+ });
747
+ const url = await getSignedUrl(s3Client, command, {
748
+ expiresIn: options?.expiresIn || 3600
749
+ });
750
+ return ok(url);
751
+ } catch (error) {
752
+ return err(
753
+ HaiStorageError.PRESIGN_FAILED,
754
+ storageM("storage_presignUploadUrlFailed"),
755
+ error
756
+ );
757
+ }
758
+ },
759
+ publicUrl(key) {
760
+ const s3Config = getConfig();
761
+ if (!s3Config.publicUrl) {
762
+ return null;
763
+ }
764
+ const fullPath = fullKey(key);
765
+ return `${s3Config.publicUrl.replace(/\/+$/, "")}/${fullPath}`;
766
+ }
767
+ };
768
+ return {
769
+ name: "s3",
770
+ file,
771
+ dir,
772
+ presign,
773
+ async connect(cfg) {
774
+ if (cfg.type !== "s3") {
775
+ return err(
776
+ HaiStorageError.CONFIG_ERROR,
777
+ storageM("storage_s3ConfigTypeError")
778
+ );
779
+ }
780
+ try {
781
+ config = cfg;
782
+ logger2.info("Connecting S3 provider", {
783
+ bucket: cfg.bucket,
784
+ region: cfg.region,
785
+ endpoint: cfg.endpoint ? sanitizeEndpointUrl(cfg.endpoint) : void 0
786
+ });
787
+ client = new S3Client({
788
+ region: cfg.region,
789
+ endpoint: cfg.endpoint,
790
+ forcePathStyle: cfg.forcePathStyle,
791
+ credentials: {
792
+ accessKeyId: cfg.accessKeyId,
793
+ secretAccessKey: cfg.secretAccessKey
794
+ }
795
+ });
796
+ await client.send(new ListObjectsV2Command({
797
+ Bucket: cfg.bucket,
798
+ MaxKeys: 1
799
+ }));
800
+ logger2.info("S3 provider connected", { bucket: cfg.bucket });
801
+ return ok(void 0);
802
+ } catch (error) {
803
+ logger2.error("S3 provider connect failed", { error });
804
+ config = null;
805
+ client = null;
806
+ return err(toStorageError2(error));
807
+ }
808
+ },
809
+ async close() {
810
+ if (client) {
811
+ client.destroy();
812
+ client = null;
813
+ config = null;
814
+ logger2.info("S3 provider disconnected");
815
+ }
816
+ },
817
+ isConnected() {
818
+ return client !== null && config !== null;
819
+ }
820
+ };
821
+ }
822
+
823
+ // src/storage-main.ts
824
+ var logger3 = core.logger.child({ module: "storage", scope: "main" });
825
+ var currentProvider = null;
826
+ var currentConfig = null;
827
+ var initInProgress = false;
828
+ function createProvider(config) {
829
+ switch (config.type) {
830
+ case "s3":
831
+ return createS3Provider();
832
+ case "local":
833
+ return createLocalProvider();
834
+ default:
835
+ throw new Error(storageM("storage_unsupportedType", { params: { type: config.type } }));
836
+ }
837
+ }
838
+ var notInitialized = core.module.createNotInitializedKit(
839
+ HaiStorageError.NOT_INITIALIZED,
840
+ () => storageM("storage_notInitialized")
841
+ );
842
+ var notInitializedFile = notInitialized.proxy();
843
+ var notInitializedDir = notInitialized.proxy();
844
+ var notInitializedPresignBase = notInitialized.proxy();
845
+ var notInitializedPresign = new Proxy(
846
+ notInitializedPresignBase,
847
+ {
848
+ get: (target, prop, receiver) => prop === "publicUrl" ? () => null : Reflect.get(target, prop, receiver)
849
+ }
850
+ );
851
+ var storage = {
852
+ /**
853
+ * 初始化存储连接。
854
+ *
855
+ * 如果当前已有活跃连接,会先自动 close 再重新初始化。
856
+ * 配置会通过 Zod Schema 校验;校验失败或连接异常会返回 `CONNECTION_FAILED`。
857
+ *
858
+ * @param config 存储配置(S3 或本地),支持省略带默认值的字段。
859
+ * @returns 成功时返回 ok(undefined);失败时返回包含错误码和消息的 err。
860
+ */
861
+ async init(config) {
862
+ if (initInProgress) {
863
+ logger3.warn("Storage init already in progress, skipping concurrent call");
864
+ return err(
865
+ HaiStorageError.OPERATION_FAILED,
866
+ storageM("storage_operationFailed", { params: { error: "Concurrent initialization detected" } })
867
+ );
868
+ }
869
+ initInProgress = true;
870
+ try {
871
+ if (currentProvider) {
872
+ logger3.warn("Storage module is already initialized, reinitializing");
873
+ await storage.close();
874
+ }
875
+ logger3.info("Initializing storage module");
876
+ const parseResult = StorageConfigSchema.safeParse(config);
877
+ if (!parseResult.success) {
878
+ logger3.error("Storage config validation failed", { error: parseResult.error.message });
879
+ return err(
880
+ HaiStorageError.CONFIG_ERROR,
881
+ storageM("storage_configError", { params: { error: parseResult.error.message } }),
882
+ parseResult.error
883
+ );
884
+ }
885
+ const parsed = parseResult.data;
886
+ const provider = createProvider(parsed);
887
+ const connectResult = await provider.connect(parsed);
888
+ if (!connectResult.success) {
889
+ logger3.error("Storage module initialization failed", {
890
+ code: connectResult.error.code,
891
+ message: connectResult.error.message
892
+ });
893
+ return connectResult;
894
+ }
895
+ currentProvider = provider;
896
+ currentConfig = parsed;
897
+ logger3.info("Storage module initialized");
898
+ return ok(void 0);
899
+ } catch (error) {
900
+ logger3.error("Storage module initialization failed", { error });
901
+ return err(
902
+ HaiStorageError.CONNECTION_FAILED,
903
+ storageM("storage_operationFailed", {
904
+ params: { error: error instanceof Error ? error.message : String(error) }
905
+ }),
906
+ error
907
+ );
908
+ } finally {
909
+ initInProgress = false;
910
+ }
911
+ },
912
+ /** 文件操作接口。未初始化时所有方法返回 NOT_INITIALIZED 错误 */
913
+ get file() {
914
+ return currentProvider?.file ?? notInitializedFile;
915
+ },
916
+ /** 目录操作接口。未初始化时所有方法返回 NOT_INITIALIZED 错误 */
917
+ get dir() {
918
+ return currentProvider?.dir ?? notInitializedDir;
919
+ },
920
+ /** 签名 URL 操作接口。未初始化时 publicUrl 返回 null,其余返回错误 */
921
+ get presign() {
922
+ return currentProvider?.presign ?? notInitializedPresign;
923
+ },
924
+ /** 当前解析后的存储配置;未初始化或已关闭时为 null */
925
+ get config() {
926
+ return currentConfig;
927
+ },
928
+ /** 是否已完成初始化 */
929
+ get isInitialized() {
930
+ return currentProvider !== null;
931
+ },
932
+ /**
933
+ * 关闭存储连接并释放资源
934
+ *
935
+ * 关闭后 file/dir/presign 操作将返回 NOT_INITIALIZED 错误。
936
+ * 重复调用不会报错。
937
+ */
938
+ async close() {
939
+ if (!currentProvider) {
940
+ currentConfig = null;
941
+ logger3.info("Storage module already closed, skipping");
942
+ return;
943
+ }
944
+ logger3.info("Closing storage module");
945
+ try {
946
+ await currentProvider.close();
947
+ logger3.info("Storage module closed");
948
+ } catch (error) {
949
+ logger3.error("Storage module close failed", { error });
950
+ } finally {
951
+ currentProvider = null;
952
+ currentConfig = null;
953
+ }
954
+ }
955
+ };
956
+
957
+ export { storage };
958
+ //# sourceMappingURL=node.js.map
959
+ //# sourceMappingURL=node.js.map