@h-ai/storage 1.0.0-beta.21 → 1.0.0-beta.23

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 CHANGED
@@ -1,20 +1,22 @@
1
- import { HaiStorageError, StorageConfigSchema, PresignUploadOptionsSchema, PresignOptionsSchema } from './chunk-FK7KRCML.js';
2
- export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-FK7KRCML.js';
3
- import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-JAJYHGLI.js';
4
- export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-JAJYHGLI.js';
1
+ import { HaiStorageError, StorageConfigSchema, PresignUploadOptionsSchema, PresignOptionsSchema } from './chunk-WOVOQXRY.js';
2
+ export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-WOVOQXRY.js';
3
+ import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-RXYTWURA.js';
4
+ export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-RXYTWURA.js';
5
5
  export { STORAGE_DELETE_MAX_KEYS, STORAGE_KEY_MAX_BYTES, StorageDeleteKeysSchema, StorageKeySchema, StoragePrefixSchema, StoragePresignExpiresInSchema, getStorageKeyIssue } from './chunk-6YG3N3CW.js';
6
6
  import process from 'node:process';
7
7
  import { core, err, ok } from '@h-ai/core';
8
8
  import { Buffer } from 'node:buffer';
9
9
  import * as crypto from 'node:crypto';
10
- import { createHash, timingSafeEqual } from 'node:crypto';
10
+ import { timingSafeEqual, createHash } from 'node:crypto';
11
11
  import * as fs from 'node:fs';
12
12
  import * as fsp from 'node:fs/promises';
13
13
  import * as path from 'node:path';
14
- import { Readable } from 'node:stream';
14
+ import { finished, Readable } from 'node:stream';
15
15
  import { pipeline } from 'node:stream/promises';
16
+ import { AsyncLocalStorage } from 'node:async_hooks';
16
17
  import { S3Client, HeadBucketCommand, CreateBucketCommand, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, CopyObjectCommand, DeleteObjectsCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
17
18
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
19
+ import { getSystemErrorMessage } from 'node:util';
18
20
 
19
21
  function toStorageDeleteResult(summary) {
20
22
  if (summary.failures.length === 0)
@@ -30,6 +32,68 @@ function toStorageDeleteResult(summary) {
30
32
  }
31
33
  };
32
34
  }
35
+ async function uploadWithSource(data, signal, write) {
36
+ const readable = data instanceof Readable ? data : void 0;
37
+ if (readable) {
38
+ const cleanup = finished(readable, { readable: true, writable: false }, () => cleanup());
39
+ }
40
+ let sourceFailure;
41
+ function failReading(cause) {
42
+ sourceFailure = new Error("Storage upload source failed", { cause });
43
+ throw sourceFailure;
44
+ }
45
+ function track(source) {
46
+ return {
47
+ [Symbol.asyncIterator]() {
48
+ let iterator;
49
+ try {
50
+ iterator = source[Symbol.asyncIterator]();
51
+ } catch (cause) {
52
+ return failReading(cause);
53
+ }
54
+ return {
55
+ async next() {
56
+ try {
57
+ return await iterator.next();
58
+ } catch (cause) {
59
+ return failReading(cause);
60
+ }
61
+ },
62
+ return: iterator.return?.bind(iterator)
63
+ };
64
+ }
65
+ };
66
+ }
67
+ try {
68
+ const input = typeof data === "string" || data instanceof Uint8Array || readable ? data : track(data);
69
+ const result = await write(input);
70
+ if (result.success)
71
+ return result;
72
+ const { cause } = result.error;
73
+ const readFailure = sourceFailure;
74
+ const failedInput = readFailure !== void 0 && cause === readFailure;
75
+ const originalCause = failedInput ? readFailure.cause : cause;
76
+ if (signal?.aborted && (Object.is(originalCause, signal.reason) || originalCause instanceof Error && originalCause.name === "AbortError")) {
77
+ return err({
78
+ ...result.error,
79
+ message: storageM("storage_uploadCanceled"),
80
+ cause: signal.reason
81
+ });
82
+ }
83
+ if (failedInput || readable?.errored && readable.errored === cause) {
84
+ return err({
85
+ ...HaiStorageError.UPLOAD_FAILED,
86
+ message: storageM("storage_uploadSourceReadFailed"),
87
+ cause: originalCause,
88
+ ...result.error.ext && { ext: result.error.ext }
89
+ });
90
+ }
91
+ return result;
92
+ } finally {
93
+ if (readable && !readable.readableEnded && !readable.destroyed)
94
+ readable.destroy();
95
+ }
96
+ }
33
97
  async function* toByteStream(data, signal) {
34
98
  signal?.throwIfAborted();
35
99
  if (typeof data === "string" || data instanceof Uint8Array) {
@@ -38,6 +102,7 @@ async function* toByteStream(data, signal) {
38
102
  }
39
103
  const iterator = data[Symbol.asyncIterator]();
40
104
  let completed = false;
105
+ let failed = false;
41
106
  try {
42
107
  while (true) {
43
108
  signal?.throwIfAborted();
@@ -63,61 +128,27 @@ async function* toByteStream(data, signal) {
63
128
  yield result.value;
64
129
  }
65
130
  signal?.throwIfAborted();
131
+ } catch (error) {
132
+ failed = true;
133
+ throw error;
66
134
  } finally {
67
135
  if (!completed && iterator.return) {
68
- if (signal?.aborted)
69
- void Promise.resolve(iterator.return()).catch(() => {
136
+ const closed = Promise.resolve().then(() => iterator.return?.());
137
+ if (failed || signal?.aborted)
138
+ void closed.catch(() => {
70
139
  });
71
140
  else
72
- await iterator.return();
141
+ await closed;
73
142
  }
74
143
  }
75
144
  }
76
145
 
77
- // src/providers/storage-operation-log.ts
78
- function normalizePayloadValue(value) {
79
- if (value instanceof Uint8Array) {
80
- return { byteLength: value.byteLength };
81
- }
82
- return value;
83
- }
84
- function stringifyOperationPayload(payload, maxLength) {
85
- if (maxLength <= 0) {
86
- return "";
87
- }
88
- try {
89
- const text = JSON.stringify(payload, (_key, value) => {
90
- if (typeof value === "bigint") {
91
- return value.toString();
92
- }
93
- if (typeof value === "function") {
94
- return "[Function]";
95
- }
96
- return normalizePayloadValue(value);
97
- });
98
- return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
99
- } catch {
100
- const text = String(payload);
101
- return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
102
- }
103
- }
104
- function logStorageOperation(logger4, config, category, operation, payload) {
105
- if (!config?.[category]) {
106
- return;
107
- }
108
- logger4[config.level]("Storage operation executed", {
109
- category,
110
- operation,
111
- payload: stringifyOperationPayload(payload, config.maxLength)
112
- });
113
- }
114
-
115
146
  // src/providers/storage-provider-local.ts
116
147
  var logger = core.logger.child({ module: "storage", scope: "provider-local" });
117
148
  var PATH_SEGMENT_SPLIT_REGEX = /[\\/]+/;
118
149
  var METADATA_DIRECTORY = ".hai-storage-meta";
119
150
  function toStorageError(error, key) {
120
- const e = error;
151
+ const e = error ?? {};
121
152
  if (e.code === "EEXIST") {
122
153
  return {
123
154
  ...HaiStorageError.ALREADY_EXISTS,
@@ -258,9 +289,6 @@ function createLocalProvider() {
258
289
  }
259
290
  return target;
260
291
  }
261
- function logOperation(category, operation, payload) {
262
- logStorageOperation(logger, config?.operationLog, category, operation, payload);
263
- }
264
292
  function metadataKey(filePath, stat2) {
265
293
  return path.relative(getConfig().root, path.join(path.dirname(filePath), METADATA_DIRECTORY, `${metadataGeneration(stat2)}.meta.json`));
266
294
  }
@@ -287,7 +315,6 @@ function createLocalProvider() {
287
315
  if (error.code !== "ENOENT")
288
316
  throw error;
289
317
  }
290
- logOperation("write", "file.put", { key, options });
291
318
  const dir2 = path.dirname(filePath);
292
319
  await fsp.mkdir(dir2, { recursive: true, mode: cfg.directoryMode });
293
320
  temporaryPath = `${filePath}.${crypto.randomUUID()}.upload.tmp`;
@@ -329,19 +356,18 @@ function createLocalProvider() {
329
356
  await fsp.unlink(previousMetadataPath);
330
357
  } catch (error) {
331
358
  if (error.code !== "ENOENT")
332
- logger.warn("Obsolete local metadata cleanup failed", { key });
359
+ logger.warn("Obsolete local metadata cleanup failed");
333
360
  }
334
361
  }
335
362
  return ok(metadata);
336
363
  } catch (error) {
337
- logger.warn("Put file failed", { key, error });
338
364
  return err(toStorageError(error, key));
339
365
  } finally {
340
366
  if (temporaryPath)
341
367
  await fsp.unlink(temporaryPath).catch(() => {
342
368
  });
343
369
  if (preparedMetadataPath && !published)
344
- await fsp.unlink(preparedMetadataPath).catch(() => logger.warn("Unpublished local metadata cleanup failed", { key }));
370
+ await fsp.unlink(preparedMetadataPath).catch(() => logger.warn("Unpublished local metadata cleanup failed"));
345
371
  }
346
372
  },
347
373
  async get(key, options = {}) {
@@ -368,7 +394,6 @@ function createLocalProvider() {
368
394
  }
369
395
  try {
370
396
  const filePath = await fullPath(key);
371
- logOperation("read", "file.getStream", { key, options });
372
397
  options.signal?.throwIfAborted();
373
398
  const stat2 = await fsp.stat(filePath);
374
399
  if (stat2.isDirectory()) {
@@ -397,7 +422,6 @@ function createLocalProvider() {
397
422
  }
398
423
  try {
399
424
  const filePath = await fullPath(key);
400
- logOperation("read", "file.head", { key });
401
425
  for (let attempt = 0; attempt < 3; attempt++) {
402
426
  const stat2 = await fsp.stat(filePath, { bigint: true });
403
427
  if (stat2.isDirectory()) {
@@ -420,17 +444,9 @@ function createLocalProvider() {
420
444
  try {
421
445
  customMetadata = JSON.parse(serializedMetadata);
422
446
  } catch {
423
- logger.warn("Invalid local metadata record", {
424
- key,
425
- generation: metadataGeneration(stat2)
426
- });
427
447
  return err(HaiStorageError.OPERATION_FAILED, storageM("storage_metadataInvalid"));
428
448
  }
429
449
  if (!isMetadataRecord(customMetadata)) {
430
- logger.warn("Invalid local metadata shape", {
431
- key,
432
- generation: metadataGeneration(stat2)
433
- });
434
450
  return err(HaiStorageError.OPERATION_FAILED, storageM("storage_metadataInvalid"));
435
451
  }
436
452
  return ok({
@@ -458,7 +474,6 @@ function createLocalProvider() {
458
474
  }
459
475
  try {
460
476
  const filePath = await fullPath(key);
461
- logOperation("read", "file.exists", { key });
462
477
  await fsp.access(filePath, fs.constants.F_OK);
463
478
  return ok(true);
464
479
  } catch (error) {
@@ -480,26 +495,22 @@ function createLocalProvider() {
480
495
  try {
481
496
  const filePath = await fullPath(key);
482
497
  const generationKey = metadataKey(filePath, await fsp.stat(filePath, { bigint: true }));
483
- logOperation("write", "file.delete", { key });
484
- logger.debug("Deleting file", { key });
485
498
  await fsp.unlink(filePath);
486
499
  try {
487
500
  await fsp.unlink(await fullPath(generationKey));
488
501
  } catch (error) {
489
502
  if (error.code !== "ENOENT")
490
- logger.warn("Deleted object metadata cleanup failed", { key });
503
+ logger.warn("Deleted object metadata cleanup failed");
491
504
  }
492
505
  return ok(void 0);
493
506
  } catch (error) {
494
507
  if (error.code === "ENOENT") {
495
508
  return ok(void 0);
496
509
  }
497
- logger.warn("Delete file failed", { key, error });
498
510
  return err(toStorageError(error, key));
499
511
  }
500
512
  },
501
513
  async deleteMany(keys) {
502
- logOperation("write", "file.deleteMany", { count: keys.length, keys });
503
514
  const summary = { deletedKeys: [], failures: [] };
504
515
  const uniqueKeys = [
505
516
  ...new Set(keys)
@@ -531,7 +542,6 @@ function createLocalProvider() {
531
542
  return toStorageDeleteResult(summary);
532
543
  },
533
544
  async copy(sourceKey, destKey, options = {}) {
534
- logOperation("write", "file.copy", { sourceKey, destKey, options });
535
545
  const source = await file.head(sourceKey);
536
546
  if (!source.success)
537
547
  return source;
@@ -554,7 +564,6 @@ function createLocalProvider() {
554
564
  if (typeof prefix !== "string" || typeof delimiter !== "string" || !Number.isInteger(maxKeys) || maxKeys < 1 || maxKeys > 1e3 || Buffer.from(prefix).toString("utf8") !== prefix || Buffer.from(delimiter).toString("utf8") !== delimiter) {
555
565
  return err(HaiStorageError.INVALID_LIST_OPTIONS, storageM("storage_listOptionsInvalid"));
556
566
  }
557
- logOperation("read", "dir.list", { options });
558
567
  safePath(cfg.root, prefix);
559
568
  const root = await fsp.realpath(cfg.root);
560
569
  const scope = crypto.createHash("sha256").update(JSON.stringify([root, prefix, delimiter])).digest("hex");
@@ -647,7 +656,6 @@ function createLocalProvider() {
647
656
  }
648
657
  try {
649
658
  const dirPath = await fullPath(prefix);
650
- logOperation("write", "dir.delete", { prefix });
651
659
  await fsp.rm(dirPath, { recursive: true, force: true });
652
660
  return ok(void 0);
653
661
  } catch (error) {
@@ -659,16 +667,13 @@ function createLocalProvider() {
659
667
  }
660
668
  };
661
669
  const presign = {
662
- async getUrl(key, options) {
663
- logOperation("read", "presign.getUrl", { key, options });
670
+ async getUrl() {
664
671
  return err(HaiStorageError.UNSUPPORTED_OPERATION, storageM("storage_localPresignUnsupported"));
665
672
  },
666
- async putUrl(key, options) {
667
- logOperation("write", "presign.putUrl", { key, options });
673
+ async putUrl() {
668
674
  return err(HaiStorageError.UNSUPPORTED_OPERATION, storageM("storage_localPresignUnsupported"));
669
675
  },
670
- publicUrl(key) {
671
- logOperation("read", "presign.publicUrl", { key });
676
+ publicUrl() {
672
677
  return null;
673
678
  }
674
679
  };
@@ -686,20 +691,19 @@ function createLocalProvider() {
686
691
  }
687
692
  try {
688
693
  config = cfg;
689
- logger.info("Connecting local provider", { root: cfg.root });
694
+ logger.debug("Connecting local provider");
690
695
  await fsp.mkdir(cfg.root, { recursive: true, mode: cfg.directoryMode });
691
696
  config = { ...cfg, root: await fsp.realpath(cfg.root) };
692
697
  connected = true;
693
- logger.info("Local provider connected", { root: cfg.root });
698
+ logger.debug("Local provider connected");
694
699
  return ok(void 0);
695
700
  } catch (error) {
696
- logger.error("Local provider connect failed", { error });
697
701
  config = null;
698
702
  return err(toStorageError(error));
699
703
  }
700
704
  },
701
705
  async close() {
702
- logger.info("Local provider disconnected");
706
+ logger.debug("Local provider disconnected");
703
707
  config = null;
704
708
  connected = false;
705
709
  },
@@ -718,8 +722,47 @@ var RETRYABLE_DELETE_CODES = /* @__PURE__ */ new Set([
718
722
  "SlowDown",
719
723
  "RequestTimeout"
720
724
  ]);
725
+ var uploadDiagnostics = new AsyncLocalStorage();
726
+ var UPLOAD_COMMANDS = /* @__PURE__ */ new Set([
727
+ "PutObject",
728
+ "CreateMultipartUpload",
729
+ "UploadPart",
730
+ "CompleteMultipartUpload",
731
+ "AbortMultipartUpload"
732
+ ]);
733
+ function observeUploadRequests(client) {
734
+ client.middlewareStack.add((next, context) => async (args) => {
735
+ const diagnostics = uploadDiagnostics.getStore();
736
+ if (!diagnostics)
737
+ return next(args);
738
+ const operation = context.commandName?.replace(/Command$/, "");
739
+ const input = args.input;
740
+ if (!operation || !UPLOAD_COMMANDS.has(operation) || !input || typeof input !== "object" || !("Bucket" in input) || input.Bucket !== diagnostics.bucket || !("Key" in input) || input.Key !== diagnostics.key) {
741
+ return next(args);
742
+ }
743
+ try {
744
+ const response = await next(args);
745
+ if (operation === "UploadPart" && (!("ETag" in response.output) || !response.output.ETag)) {
746
+ const { httpStatusCode, requestId } = response.output.$metadata;
747
+ const partNumber = "PartNumber" in input && typeof input.PartNumber === "number" ? input.PartNumber : void 0;
748
+ throw Object.assign(new Error("S3 upload part response is missing ETag"), {
749
+ name: "MissingETag",
750
+ partNumber,
751
+ $metadata: { httpStatusCode, requestId }
752
+ });
753
+ }
754
+ return response;
755
+ } catch (cause) {
756
+ if (operation === "AbortMultipartUpload")
757
+ diagnostics.cleanup = { cause };
758
+ else
759
+ diagnostics.failure ??= { cause, operation };
760
+ throw cause;
761
+ }
762
+ }, { step: "initialize", name: "haiStorageObserveUploadRequests" });
763
+ }
721
764
  function toStorageError2(error, key) {
722
- const e = error;
765
+ const e = error ?? {};
723
766
  if (e.name === "PreconditionFailed" || e.$metadata?.httpStatusCode === 412) {
724
767
  return {
725
768
  ...HaiStorageError.ALREADY_EXISTS,
@@ -738,7 +781,7 @@ function toStorageError2(error, key) {
738
781
  cause: error
739
782
  };
740
783
  }
741
- if (e.name === "AccessDenied" || e.Code === "AccessDenied") {
784
+ if (e.name === "AccessDenied" || e.Code === "AccessDenied" || e.$metadata?.httpStatusCode === 403) {
742
785
  return {
743
786
  ...HaiStorageError.PERMISSION_DENIED,
744
787
  message: storageM("storage_permissionDenied", {
@@ -843,9 +886,6 @@ function createS3Provider() {
843
886
  function fullKey(key) {
844
887
  return withPrefix(key, getConfig().prefix);
845
888
  }
846
- function logOperation(category, operation, payload) {
847
- logStorageOperation(logger2, config?.operationLog, category, operation, payload);
848
- }
849
889
  const file = {
850
890
  async put(key, data, options = {}) {
851
891
  try {
@@ -865,19 +905,19 @@ function createS3Provider() {
865
905
  ContentDisposition: options.contentDisposition,
866
906
  IfNoneMatch: options.ifNoneMatch
867
907
  };
868
- logOperation("write", "file.put", {
869
- key,
870
- fullPath,
871
- byteLength: buffered ? size : void 0,
872
- options
873
- });
874
908
  if (buffered) {
875
909
  await s3Client.send(new PutObjectCommand({ ...params, Body: body }), { abortSignal: options.signal });
876
910
  } else {
911
+ const diagnostics = { bucket: s3Config.bucket, key: fullPath };
877
912
  async function* counted() {
878
- for await (const chunk of toByteStream(data, options.signal)) {
879
- size += chunk.byteLength;
880
- yield chunk;
913
+ try {
914
+ for await (const chunk of toByteStream(data, options.signal)) {
915
+ size += chunk.byteLength;
916
+ yield chunk;
917
+ }
918
+ } catch (cause) {
919
+ diagnostics.failure ??= { cause };
920
+ throw cause;
881
921
  }
882
922
  }
883
923
  const stream = Readable.from(counted(), { objectMode: false });
@@ -888,13 +928,32 @@ function createS3Provider() {
888
928
  if (options.signal?.aborted)
889
929
  abort();
890
930
  const { Upload } = await import('@aws-sdk/lib-storage');
891
- await new Upload({
931
+ const upload = new Upload({
892
932
  client: s3Client,
893
933
  params: { ...params, Body: stream },
894
- queueSize: 2,
934
+ partSize: s3Config.multipart.partSizeBytes,
935
+ queueSize: s3Config.multipart.queueSize,
895
936
  leavePartsOnError: false,
896
937
  abortController: controller
897
- }).done();
938
+ });
939
+ await uploadDiagnostics.run(diagnostics, () => upload.done());
940
+ } catch (cause) {
941
+ const failure = diagnostics.failure;
942
+ const cleanup = diagnostics.cleanup;
943
+ const original = cleanup && failure && Object.is(cause, cleanup.cause) ? failure.cause : cause;
944
+ const error = toStorageError2(original, key);
945
+ const s3Operation = failure && Object.is(failure.cause, original) ? failure.operation : void 0;
946
+ if (s3Operation || cleanup) {
947
+ error.ext = {
948
+ ...error.ext,
949
+ s3Operation,
950
+ ...cleanup && {
951
+ cleanupPending: true,
952
+ cleanupError: toStorageError2(cleanup.cause)
953
+ }
954
+ };
955
+ }
956
+ return err(error);
898
957
  } finally {
899
958
  options.signal?.removeEventListener("abort", abort);
900
959
  stream.destroy();
@@ -932,7 +991,6 @@ function createS3Provider() {
932
991
  const s3Client = getClient();
933
992
  const s3Config = getConfig();
934
993
  const fullPath = fullKey(key);
935
- logOperation("read", "file.getStream", { key, fullPath, options });
936
994
  let Range;
937
995
  if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
938
996
  const start = options.rangeStart ?? 0;
@@ -961,7 +1019,6 @@ function createS3Provider() {
961
1019
  const s3Client = getClient();
962
1020
  const s3Config = getConfig();
963
1021
  const fullPath = fullKey(key);
964
- logOperation("read", "file.head", { key, fullPath });
965
1022
  const response = await s3Client.send(new HeadObjectCommand({
966
1023
  Bucket: s3Config.bucket,
967
1024
  Key: fullPath
@@ -979,7 +1036,6 @@ function createS3Provider() {
979
1036
  }
980
1037
  },
981
1038
  async exists(key) {
982
- logOperation("read", "file.exists", { key });
983
1039
  const result = await file.head(key);
984
1040
  if (result.success) {
985
1041
  return ok(true);
@@ -994,7 +1050,6 @@ function createS3Provider() {
994
1050
  const s3Client = getClient();
995
1051
  const s3Config = getConfig();
996
1052
  const fullPath = fullKey(key);
997
- logOperation("write", "file.delete", { key, fullPath });
998
1053
  await s3Client.send(new DeleteObjectCommand({
999
1054
  Bucket: s3Config.bucket,
1000
1055
  Key: fullPath
@@ -1008,7 +1063,6 @@ function createS3Provider() {
1008
1063
  try {
1009
1064
  const s3Client = getClient("deleteMany");
1010
1065
  const s3Config = getConfig();
1011
- logOperation("write", "file.deleteMany", { count: keys.length, keys });
1012
1066
  const uniqueKeys = [
1013
1067
  ...new Set(keys)
1014
1068
  ];
@@ -1110,13 +1164,6 @@ function createS3Provider() {
1110
1164
  s3Config.bucket,
1111
1165
  ...sourceFullPath.split("/")
1112
1166
  ].map((part) => encodeURIComponent(part).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
1113
- logOperation("write", "file.copy", {
1114
- sourceKey,
1115
- destKey,
1116
- sourceFullPath,
1117
- destFullPath,
1118
- options
1119
- });
1120
1167
  await s3Client.send(new CopyObjectCommand({
1121
1168
  Bucket: s3Config.bucket,
1122
1169
  CopySource: copySource,
@@ -1136,7 +1183,6 @@ function createS3Provider() {
1136
1183
  try {
1137
1184
  const s3Client = getClient();
1138
1185
  const s3Config = getConfig();
1139
- logOperation("read", "dir.list", { options });
1140
1186
  const response = await s3Client.send(new ListObjectsV2Command({
1141
1187
  Bucket: s3Config.bucket,
1142
1188
  Prefix: withPrefix(options.prefix ?? "", s3Config.prefix) || void 0,
@@ -1165,7 +1211,6 @@ function createS3Provider() {
1165
1211
  },
1166
1212
  async delete(prefix) {
1167
1213
  try {
1168
- logOperation("write", "dir.delete", { prefix });
1169
1214
  let continuationToken;
1170
1215
  do {
1171
1216
  const listResult = await dir.list({
@@ -1198,7 +1243,6 @@ function createS3Provider() {
1198
1243
  const s3Client = getClient("presign");
1199
1244
  const s3Config = getConfig();
1200
1245
  const fullPath = fullKey(key);
1201
- logOperation("read", "presign.getUrl", { key, fullPath, options });
1202
1246
  const basePath = publicBasePath;
1203
1247
  const command = new GetObjectCommand({
1204
1248
  Bucket: s3Config.bucket,
@@ -1224,7 +1268,6 @@ function createS3Provider() {
1224
1268
  const s3Client = getClient("presign");
1225
1269
  const s3Config = getConfig();
1226
1270
  const fullPath = fullKey(key);
1227
- logOperation("write", "presign.putUrl", { key, fullPath, options });
1228
1271
  const basePath = publicBasePath;
1229
1272
  const command = new PutObjectCommand({
1230
1273
  Bucket: s3Config.bucket,
@@ -1249,7 +1292,6 @@ function createS3Provider() {
1249
1292
  return null;
1250
1293
  }
1251
1294
  const fullPath = fullKey(key);
1252
- logOperation("read", "presign.publicUrl", { key, fullPath });
1253
1295
  return `${s3Config.publicUrl.replace(TRAILING_SLASHES_REGEX, "")}/${fullPath}`;
1254
1296
  }
1255
1297
  };
@@ -1268,11 +1310,7 @@ function createS3Provider() {
1268
1310
  try {
1269
1311
  await closeClients();
1270
1312
  config = cfg;
1271
- logger2.info("Connecting S3 provider", {
1272
- bucket: cfg.bucket,
1273
- region: cfg.region,
1274
- endpoint: cfg.endpoint ? core.sanitize.sanitizeUrl(cfg.endpoint) : void 0
1275
- });
1313
+ logger2.debug("Connecting S3 provider");
1276
1314
  const clientConfig = {
1277
1315
  region: cfg.region,
1278
1316
  endpoint: cfg.endpoint,
@@ -1286,6 +1324,7 @@ function createS3Provider() {
1286
1324
  }
1287
1325
  };
1288
1326
  client = new S3Client(clientConfig);
1327
+ observeUploadRequests(client);
1289
1328
  deleteClient = new S3Client({ ...clientConfig, maxAttempts: 1 });
1290
1329
  if (cfg.publicEndpoint) {
1291
1330
  const endpoint = new URL(cfg.publicEndpoint);
@@ -1311,7 +1350,7 @@ function createS3Provider() {
1311
1350
  CreateBucketConfiguration: { LocationConstraint: cfg.region }
1312
1351
  }
1313
1352
  }));
1314
- logger2.info("S3 bucket created", { bucket: cfg.bucket });
1353
+ logger2.debug("S3 bucket created");
1315
1354
  } catch (createError) {
1316
1355
  if (!(createError instanceof Error && createError.name === "BucketAlreadyOwnedByYou"))
1317
1356
  throw createError;
@@ -1326,10 +1365,9 @@ function createS3Provider() {
1326
1365
  }));
1327
1366
  config = cfg;
1328
1367
  ready = true;
1329
- logger2.info("S3 provider connected", { bucket: cfg.bucket });
1368
+ logger2.debug("S3 provider connected");
1330
1369
  return ok(void 0);
1331
1370
  } catch (error) {
1332
- logger2.error("S3 provider connect failed", { error });
1333
1371
  const original = toStorageError2(error);
1334
1372
  try {
1335
1373
  await closeClients();
@@ -1385,9 +1423,136 @@ var checksum = {
1385
1423
  return ok(actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes));
1386
1424
  }
1387
1425
  };
1426
+ var logger3 = core.logger.child({ module: "storage", scope: "operations" });
1427
+ var ERROR_NAME = /^[\w:-]{1,80}$/;
1428
+ var FAILURE_REASONS = {
1429
+ [HaiStorageError.CONNECTION_FAILED.code]: "Storage connection or cleanup failed",
1430
+ [HaiStorageError.OPERATION_FAILED.code]: "Storage operation failed",
1431
+ [HaiStorageError.NOT_FOUND.code]: "Storage object was not found",
1432
+ [HaiStorageError.ALREADY_EXISTS.code]: "Destination object already exists",
1433
+ [HaiStorageError.PERMISSION_DENIED.code]: "Storage access was denied",
1434
+ [HaiStorageError.QUOTA_EXCEEDED.code]: "Storage quota or disk space was exhausted",
1435
+ [HaiStorageError.INVALID_PATH.code]: "Storage path was invalid",
1436
+ [HaiStorageError.IO_ERROR.code]: "Storage read or write failed",
1437
+ [HaiStorageError.NETWORK_ERROR.code]: "Storage network request failed",
1438
+ [HaiStorageError.NOT_INITIALIZED.code]: "Storage provider was not initialized",
1439
+ [HaiStorageError.UNSUPPORTED_TYPE.code]: "Storage provider type was unsupported",
1440
+ [HaiStorageError.CONFIG_ERROR.code]: "Storage configuration was invalid",
1441
+ [HaiStorageError.PRESIGN_FAILED.code]: "Could not sign a storage URL",
1442
+ [HaiStorageError.UPLOAD_FAILED.code]: "Could not read the upload source",
1443
+ [HaiStorageError.DOWNLOAD_FAILED.code]: "Storage download failed",
1444
+ [HaiStorageError.CHECKSUM_FAILED.code]: "Could not calculate the checksum",
1445
+ [HaiStorageError.INVALID_CHECKSUM.code]: "Checksum input was invalid",
1446
+ [HaiStorageError.UNSUPPORTED_OPERATION.code]: "Storage provider does not support this operation",
1447
+ [HaiStorageError.INVALID_LIST_OPTIONS.code]: "Storage list options or cursor were invalid"
1448
+ };
1449
+ function storageErrorContext(code, cause, cancelled = false) {
1450
+ const details = cause && typeof cause === "object" ? cause : {};
1451
+ const name = "name" in details ? details.name : void 0;
1452
+ const causeCode = "code" in details && typeof details.code === "string" ? details.code : name;
1453
+ const errno = "errno" in details ? details.errno : void 0;
1454
+ const metadata = "$metadata" in details ? details.$metadata : void 0;
1455
+ const status = typeof metadata === "object" && metadata !== null && "httpStatusCode" in metadata ? metadata.httpStatusCode : void 0;
1456
+ const requestId = typeof metadata === "object" && metadata !== null && "requestId" in metadata ? metadata.requestId : void 0;
1457
+ const partNumber = "partNumber" in details ? details.partNumber : void 0;
1458
+ return {
1459
+ code,
1460
+ reason: cancelled || name === "AbortError" || causeCode === "ABORT_ERR" ? "Storage operation was cancelled" : FAILURE_REASONS[code] ?? "Storage operation failed",
1461
+ causeCode: typeof causeCode === "string" && ERROR_NAME.test(causeCode) ? causeCode : void 0,
1462
+ systemReason: typeof errno === "number" && Number.isInteger(errno) && errno < 0 && errno >= -2147483648 ? getSystemErrorMessage(errno) : void 0,
1463
+ httpStatus: typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? status : void 0,
1464
+ providerRequestId: typeof requestId === "string" && ERROR_NAME.test(requestId) ? requestId : void 0,
1465
+ partNumber: typeof partNumber === "number" && Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 1e4 ? partNumber : void 0
1466
+ };
1467
+ }
1468
+ function warnFailure(operation, failure, signal) {
1469
+ const batch = failure.ext?.deleteMany;
1470
+ const s3Operation = failure.ext?.s3Operation;
1471
+ const cleanup = failure.ext?.cleanupError;
1472
+ const cleanupContext = cleanup && typeof cleanup === "object" && "code" in cleanup && typeof cleanup.code === "string" && ERROR_NAME.test(cleanup.code) ? storageErrorContext(cleanup.code, "cause" in cleanup ? cleanup.cause : void 0) : void 0;
1473
+ const failures = batch && typeof batch === "object" && "failures" in batch && Array.isArray(batch.failures) ? batch.failures : void 0;
1474
+ logger3.warn("Storage operation failed", {
1475
+ operation,
1476
+ ...storageErrorContext(failure.code, failure.cause, signal?.aborted && Object.is(failure.cause, signal.reason)),
1477
+ s3Operation: typeof s3Operation === "string" && ERROR_NAME.test(s3Operation) ? s3Operation : void 0,
1478
+ cleanupError: cleanupContext,
1479
+ failedCount: failures?.length,
1480
+ failureCodes: failures && [
1481
+ ...new Set(failures.slice(0, 32).flatMap((item) => {
1482
+ if (typeof item === "object" && item !== null && "code" in item && typeof item.code === "string" && ERROR_NAME.test(item.code))
1483
+ return [item.code];
1484
+ return [];
1485
+ }))
1486
+ ]
1487
+ });
1488
+ }
1489
+ async function logStorageResult(operation, pending, signal) {
1490
+ const result = await pending.catch((cause) => err(HaiStorageError.OPERATION_FAILED, storageM("storage_operationFailed", {
1491
+ params: {
1492
+ error: cause instanceof Error ? cause.message : storageM("storage_unknownError")
1493
+ }
1494
+ }), cause));
1495
+ if (!result.success)
1496
+ warnFailure(operation, result.error, signal);
1497
+ return result;
1498
+ }
1499
+ async function* logReadStream(stream) {
1500
+ try {
1501
+ yield* stream;
1502
+ } catch (cause) {
1503
+ warnFailure("file.getStream", { code: HaiStorageError.DOWNLOAD_FAILED.code, cause });
1504
+ throw cause;
1505
+ }
1506
+ }
1507
+ function debugOperation(config, category, operation, summary) {
1508
+ if (!config?.[category])
1509
+ return;
1510
+ const text = summary && config.maxLength > 0 ? JSON.stringify(summary) : "";
1511
+ const payload = text.length > config.maxLength ? `${text.slice(0, config.maxLength)}...` : text;
1512
+ logger3.debug("Storage operation started", { category, operation, payload });
1513
+ }
1514
+ function withStorageOperationLogs(provider, config) {
1515
+ const { file, dir, presign } = provider;
1516
+ function run(category, operation, invoke, summary) {
1517
+ debugOperation(config, category, operation, summary);
1518
+ return logStorageResult(operation, invoke());
1519
+ }
1520
+ return {
1521
+ file: {
1522
+ put(key, data, options) {
1523
+ debugOperation(config, "write", "file.put", {
1524
+ byteLength: typeof data === "string" ? Buffer.byteLength(data) : data instanceof Uint8Array ? data.byteLength : void 0
1525
+ });
1526
+ return logStorageResult("file.put", uploadWithSource(data, options?.signal, (input) => file.put(key, input, options)), options?.signal);
1527
+ },
1528
+ get: (key, options) => run("read", "file.get", () => file.get(key, options)),
1529
+ async getStream(key, options) {
1530
+ const result = await run("read", "file.getStream", () => file.getStream(key, options));
1531
+ return result.success ? ok(logReadStream(result.data)) : result;
1532
+ },
1533
+ head: (key) => run("read", "file.head", () => file.head(key)),
1534
+ exists: (key) => run("read", "file.exists", () => file.exists(key)),
1535
+ delete: (key) => run("write", "file.delete", () => file.delete(key)),
1536
+ deleteMany: (keys) => run("write", "file.deleteMany", () => file.deleteMany(keys), { count: keys.length }),
1537
+ copy: (source, destination, options) => run("write", "file.copy", () => file.copy(source, destination, options))
1538
+ },
1539
+ dir: {
1540
+ list: (options) => run("read", "dir.list", () => dir.list(options), { maxKeys: options?.maxKeys }),
1541
+ delete: (prefix) => run("write", "dir.delete", () => dir.delete(prefix))
1542
+ },
1543
+ presign: {
1544
+ getUrl: (key, options) => run("read", "presign.getUrl", () => presign.getUrl(key, options)),
1545
+ putUrl: (key, options) => run("write", "presign.putUrl", () => presign.putUrl(key, options)),
1546
+ publicUrl(key) {
1547
+ debugOperation(config, "read", "presign.publicUrl");
1548
+ return presign.publicUrl(key);
1549
+ }
1550
+ }
1551
+ };
1552
+ }
1388
1553
 
1389
1554
  // src/storage-main.ts
1390
- var logger3 = core.logger.child({ module: "storage", scope: "main" });
1555
+ var logger4 = core.logger.child({ module: "storage", scope: "main" });
1391
1556
  var currentProvider = null;
1392
1557
  var currentConfig = null;
1393
1558
  var initInProgress = false;
@@ -1407,7 +1572,7 @@ async function closeOwnedProvider() {
1407
1572
  pendingProvider = null;
1408
1573
  return ok(void 0);
1409
1574
  } catch (error) {
1410
- logger3.error("Provider cleanup failed; retaining resource for retry", { error });
1575
+ logger4.error("Provider cleanup failed; retaining resource for retry", storageErrorContext(HaiStorageError.CONNECTION_FAILED.code, error));
1411
1576
  return err({
1412
1577
  ...HaiStorageError.CONNECTION_FAILED,
1413
1578
  message: storageM("storage_operationFailed", {
@@ -1463,8 +1628,16 @@ var notInitializedPresign = new Proxy(
1463
1628
  get: (target, prop, receiver) => prop === "publicUrl" ? () => null : Reflect.get(target, prop, receiver)
1464
1629
  }
1465
1630
  );
1631
+ var unavailableOperations = withStorageOperationLogs({
1632
+ file: notInitializedFile,
1633
+ dir: notInitializedDir,
1634
+ presign: notInitializedPresign
1635
+ });
1466
1636
  var storage = {
1467
- checksum,
1637
+ checksum: {
1638
+ generate: (data, options) => logStorageResult("checksum.generate", checksum.generate(data, options)),
1639
+ verify: (data, expected, options) => logStorageResult("checksum.verify", checksum.verify(data, expected, options))
1640
+ },
1468
1641
  /**
1469
1642
  * 初始化存储连接。
1470
1643
  *
@@ -1485,7 +1658,7 @@ var storage = {
1485
1658
  */
1486
1659
  async init(config) {
1487
1660
  if (initInProgress || closingPromise) {
1488
- logger3.warn("Storage init already in progress, skipping concurrent call");
1661
+ logger4.error("Storage initialization refused during an existing lifecycle operation", { code: HaiStorageError.OPERATION_FAILED.code });
1489
1662
  return err(
1490
1663
  HaiStorageError.OPERATION_FAILED,
1491
1664
  storageM("storage_operationFailed", {
@@ -1502,10 +1675,10 @@ var storage = {
1502
1675
  const cleanup = await closeOwnedProvider();
1503
1676
  if (!cleanup.success)
1504
1677
  return cleanup;
1505
- logger3.info("Initializing storage module");
1678
+ logger4.debug("Initializing storage module");
1506
1679
  const parseResult = StorageConfigSchema.safeParse(config);
1507
1680
  if (!parseResult.success) {
1508
- logger3.error("Storage config validation failed", { error: parseResult.error.message });
1681
+ logger4.error("Storage config validation failed", { code: HaiStorageError.CONFIG_ERROR.code, issueCount: parseResult.error.issues.length });
1509
1682
  return err(
1510
1683
  HaiStorageError.CONFIG_ERROR,
1511
1684
  storageM("storage_configError", {
@@ -1515,25 +1688,30 @@ var storage = {
1515
1688
  );
1516
1689
  }
1517
1690
  const parsed = parseResult.data;
1518
- if (core.module.requiresSharedState(process.env) && parsed.type === "local")
1691
+ if (core.module.requiresSharedState(process.env) && parsed.type === "local") {
1692
+ logger4.error("Storage initialization requires a shared provider", { code: HaiStorageError.CONFIG_ERROR.code, provider: parsed.type });
1519
1693
  return err(HaiStorageError.CONFIG_ERROR, storageM("storage_sharedBackendRequired"));
1694
+ }
1520
1695
  const provider = createProvider(parsed);
1521
1696
  pendingProvider = provider;
1522
1697
  const connectResult = await provider.connect(parsed);
1523
1698
  if (!connectResult.success) {
1524
- logger3.error("Storage module initialization failed", {
1525
- code: connectResult.error.code,
1526
- message: connectResult.error.message
1699
+ logger4.error("Storage module initialization failed", {
1700
+ provider: provider.name,
1701
+ ...storageErrorContext(connectResult.error.code, connectResult.error.cause)
1527
1702
  });
1528
1703
  return await failInitialization(connectResult);
1529
1704
  }
1705
+ currentProvider = {
1706
+ ...provider,
1707
+ ...withStorageOperationLogs(provider, parsed.operationLog)
1708
+ };
1530
1709
  pendingProvider = null;
1531
- currentProvider = provider;
1532
1710
  currentConfig = parsed;
1533
- logger3.info("Storage module initialized");
1711
+ logger4.debug("Storage module initialized");
1534
1712
  return ok(void 0);
1535
1713
  } catch (error) {
1536
- logger3.error("Storage module initialization failed", { error });
1714
+ logger4.error("Storage module initialization failed", storageErrorContext(HaiStorageError.CONNECTION_FAILED.code, error));
1537
1715
  return await failInitialization(err(
1538
1716
  HaiStorageError.CONNECTION_FAILED,
1539
1717
  storageM("storage_operationFailed", {
@@ -1551,15 +1729,15 @@ var storage = {
1551
1729
  },
1552
1730
  /** 文件操作接口。未初始化时所有方法返回 NOT_INITIALIZED 错误 */
1553
1731
  get file() {
1554
- return currentProvider?.file ?? notInitializedFile;
1732
+ return currentProvider?.file ?? unavailableOperations.file;
1555
1733
  },
1556
1734
  /** 目录操作接口。未初始化时所有方法返回 NOT_INITIALIZED 错误 */
1557
1735
  get dir() {
1558
- return currentProvider?.dir ?? notInitializedDir;
1736
+ return currentProvider?.dir ?? unavailableOperations.dir;
1559
1737
  },
1560
1738
  /** 签名 URL 操作接口。未初始化时 publicUrl 返回 null,其余返回错误 */
1561
1739
  get presign() {
1562
- return currentProvider?.presign ?? notInitializedPresign;
1740
+ return currentProvider?.presign ?? unavailableOperations.presign;
1563
1741
  },
1564
1742
  /** 当前脱敏后的存储配置快照;未初始化或已关闭时为 null */
1565
1743
  get config() {