@h-ai/storage 0.1.0-alpha.60 → 0.1.0-alpha.62

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,16 +1,63 @@
1
- import { HaiStorageError, StorageConfigSchema } from './chunk-W44BX2GI.js';
2
- export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-W44BX2GI.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, ok, err } 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';
1
+ import { HaiStorageError, StorageConfigSchema } from './chunk-KXE6OO5T.js';
2
+ export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-KXE6OO5T.js';
3
+ import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-25GYTRA7.js';
4
+ export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-25GYTRA7.js';
5
+ import { core, err, ok } from '@h-ai/core';
6
+ import { Buffer } from 'node:buffer';
7
+ import * as crypto from 'node:crypto';
8
+ import { createHash, timingSafeEqual } from 'node:crypto';
9
+ import * as fs from 'node:fs';
10
+ import * as fsp from 'node:fs/promises';
11
+ import * as path from 'node:path';
12
+ import { Readable } from 'node:stream';
13
+ import { pipeline } from 'node:stream/promises';
11
14
  import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, CopyObjectCommand, DeleteObjectsCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
12
15
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
13
16
 
17
+ async function* toByteStream(data, signal) {
18
+ signal?.throwIfAborted();
19
+ if (typeof data === "string" || data instanceof Uint8Array) {
20
+ yield typeof data === "string" ? Buffer.from(data) : data;
21
+ return;
22
+ }
23
+ const iterator = data[Symbol.asyncIterator]();
24
+ let completed = false;
25
+ try {
26
+ while (true) {
27
+ signal?.throwIfAborted();
28
+ let result;
29
+ let onAbort;
30
+ try {
31
+ const cancelled = signal ? new Promise((_, reject) => {
32
+ onAbort = () => reject(signal.reason);
33
+ signal.addEventListener("abort", onAbort, { once: true });
34
+ }) : void 0;
35
+ const next = iterator.next();
36
+ result = await (cancelled ? Promise.race([next, cancelled]) : next);
37
+ } finally {
38
+ if (onAbort)
39
+ signal?.removeEventListener("abort", onAbort);
40
+ }
41
+ if (result.done) {
42
+ completed = true;
43
+ break;
44
+ }
45
+ if (!(result.value instanceof Uint8Array))
46
+ throw new TypeError(storageM("storage_streamInvalidChunk"));
47
+ yield result.value;
48
+ }
49
+ signal?.throwIfAborted();
50
+ } finally {
51
+ if (!completed && iterator.return) {
52
+ if (signal?.aborted)
53
+ void Promise.resolve(iterator.return()).catch(() => {
54
+ });
55
+ else
56
+ await iterator.return();
57
+ }
58
+ }
59
+ }
60
+
14
61
  // src/providers/storage-operation-log.ts
15
62
  function normalizePayloadValue(value) {
16
63
  if (value instanceof Uint8Array) {
@@ -54,6 +101,9 @@ var logger = core.logger.child({ module: "storage", scope: "provider-local" });
54
101
  var PATH_SEGMENT_SPLIT_REGEX = /[\\/]+/;
55
102
  function toStorageError(error, key) {
56
103
  const e = error;
104
+ if (e.code === "EEXIST") {
105
+ return { ...HaiStorageError.ALREADY_EXISTS, message: storageM("storage_fileAlreadyExists", { params: { key: key ?? "" } }), cause: error };
106
+ }
57
107
  if (e.code === "ENOENT") {
58
108
  return {
59
109
  ...HaiStorageError.NOT_FOUND,
@@ -103,7 +153,7 @@ function calculateEtag(stat2) {
103
153
  return `"${stat2.size.toString(16)}-${stat2.mtimeMs.toString(16)}"`;
104
154
  }
105
155
  function isMetaFile(key) {
106
- return key.endsWith(".meta.json");
156
+ return key.endsWith(".meta.json") || key.endsWith(".upload.tmp");
107
157
  }
108
158
  function createInvalidPathError() {
109
159
  const error = new Error(storageM("storage_pathTraversal"));
@@ -145,15 +195,6 @@ function createLocalProvider() {
145
195
  function logOperation(category, operation, payload) {
146
196
  logStorageOperation(logger, config?.operationLog, category, operation, payload);
147
197
  }
148
- function toBuffer(data) {
149
- if (typeof data === "string") {
150
- return Buffer.from(data);
151
- }
152
- if (data instanceof Buffer) {
153
- return data;
154
- }
155
- return Buffer.from(data);
156
- }
157
198
  const file = {
158
199
  async put(key, data, options = {}) {
159
200
  if (isMetaFile(key)) {
@@ -162,14 +203,23 @@ function createLocalProvider() {
162
203
  storageM("storage_metaFileAccess", { params: { key } })
163
204
  );
164
205
  }
206
+ let temporaryPath;
165
207
  try {
166
208
  const filePath = fullPath(key);
167
- const buffer = toBuffer(data);
168
- logOperation("write", "file.put", { key, byteLength: buffer.length, options });
169
- logger.debug("Putting file", { key, size: buffer.length });
209
+ logOperation("write", "file.put", { key, options });
170
210
  const dir2 = path.dirname(filePath);
171
211
  await fsp.mkdir(dir2, { recursive: true, mode: getConfig().directoryMode });
172
- await fsp.writeFile(filePath, buffer, { mode: getConfig().fileMode });
212
+ temporaryPath = `${filePath}.${crypto.randomUUID()}.upload.tmp`;
213
+ await pipeline(
214
+ Readable.from(toByteStream(data, options.signal), { objectMode: false }),
215
+ fs.createWriteStream(temporaryPath, { flags: "wx", mode: getConfig().fileMode }),
216
+ { signal: options.signal }
217
+ );
218
+ options.signal?.throwIfAborted();
219
+ if (options.ifNoneMatch === "*")
220
+ await fsp.link(temporaryPath, filePath);
221
+ else
222
+ await fsp.rename(temporaryPath, filePath);
173
223
  const stat2 = await fsp.stat(filePath);
174
224
  const metadata = {
175
225
  key,
@@ -179,7 +229,7 @@ function createLocalProvider() {
179
229
  etag: calculateEtag(stat2),
180
230
  metadata: options.metadata
181
231
  };
182
- if (options.metadata) {
232
+ if (options.metadata || options.contentType) {
183
233
  const metaPath = `${filePath}.meta.json`;
184
234
  await fsp.writeFile(metaPath, JSON.stringify({
185
235
  contentType: options.contentType,
@@ -190,9 +240,26 @@ function createLocalProvider() {
190
240
  } catch (error) {
191
241
  logger.warn("Put file failed", { key, error });
192
242
  return err(toStorageError(error, key));
243
+ } finally {
244
+ if (temporaryPath)
245
+ await fsp.unlink(temporaryPath).catch(() => {
246
+ });
193
247
  }
194
248
  },
195
249
  async get(key, options = {}) {
250
+ const streamed = await file.getStream(key, options);
251
+ if (!streamed.success)
252
+ return streamed;
253
+ try {
254
+ const chunks = [];
255
+ for await (const chunk of streamed.data)
256
+ chunks.push(chunk);
257
+ return ok(Buffer.concat(chunks));
258
+ } catch (error) {
259
+ return err(toStorageError(error, key));
260
+ }
261
+ },
262
+ async getStream(key, options = {}) {
196
263
  if (isMetaFile(key)) {
197
264
  return err(
198
265
  HaiStorageError.INVALID_PATH,
@@ -201,21 +268,17 @@ function createLocalProvider() {
201
268
  }
202
269
  try {
203
270
  const filePath = fullPath(key);
204
- logOperation("read", "file.get", { key, options });
205
- if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
206
- const stat2 = await fsp.stat(filePath);
207
- const start = options.rangeStart ?? 0;
208
- const end = options.rangeEnd !== void 0 ? options.rangeEnd : stat2.size - 1;
209
- return new Promise((resolve2) => {
210
- const chunks = [];
211
- const stream = fs.createReadStream(filePath, { start, end });
212
- stream.on("data", (chunk) => chunks.push(chunk));
213
- stream.on("end", () => resolve2(ok(Buffer.concat(chunks))));
214
- stream.on("error", (error) => resolve2(err(toStorageError(error, key))));
215
- });
271
+ logOperation("read", "file.getStream", { key, options });
272
+ options.signal?.throwIfAborted();
273
+ const stat2 = await fsp.stat(filePath);
274
+ if (stat2.isDirectory())
275
+ return err(HaiStorageError.INVALID_PATH, storageM("storage_pathIsDir", { params: { key } }));
276
+ async function* read() {
277
+ const stream = fs.createReadStream(filePath, { start: options.rangeStart, end: options.rangeEnd, signal: options.signal });
278
+ for await (const chunk of stream)
279
+ yield chunk;
216
280
  }
217
- const data = await fsp.readFile(filePath);
218
- return ok(data);
281
+ return ok(read());
219
282
  } catch (error) {
220
283
  return err(toStorageError(error, key));
221
284
  }
@@ -379,7 +442,7 @@ function createLocalProvider() {
379
442
  return;
380
443
  const fullEntryPath = path.join(dirPath, entry.name);
381
444
  const key = path.relative(cfg.root, fullEntryPath).replace(/\\/g, "/");
382
- if (entry.name.endsWith(".meta.json"))
445
+ if (isMetaFile(entry.name))
383
446
  continue;
384
447
  if (entry.isDirectory()) {
385
448
  if (delimiter) {
@@ -494,6 +557,8 @@ function sanitizeEndpointUrl(url) {
494
557
  }
495
558
  function toStorageError2(error, key) {
496
559
  const e = error;
560
+ if (e.name === "PreconditionFailed" || e.$metadata?.httpStatusCode === 412)
561
+ return { ...HaiStorageError.ALREADY_EXISTS, message: storageM("storage_fileAlreadyExists", { params: { key: key ?? "" } }), cause: error };
497
562
  if (e.name === "NoSuchKey" || e.Code === "NoSuchKey" || e.name === "NotFound" || e.Code === "NotFound" || e.name === "404" || e.$metadata?.httpStatusCode === 404) {
498
563
  return {
499
564
  ...HaiStorageError.NOT_FOUND,
@@ -569,24 +634,50 @@ function createS3Provider() {
569
634
  const s3Client = getClient();
570
635
  const s3Config = getConfig();
571
636
  const fullPath = fullKey(key);
637
+ options.signal?.throwIfAborted();
572
638
  const body = typeof data === "string" ? Buffer.from(data) : data;
573
- logOperation("write", "file.put", { key, fullPath, byteLength: body.length, options });
574
- await s3Client.send(new PutObjectCommand({
639
+ const buffered = body instanceof Uint8Array;
640
+ let size = buffered ? body.byteLength : 0;
641
+ const params = {
575
642
  Bucket: s3Config.bucket,
576
643
  Key: fullPath,
577
- Body: body,
578
644
  ContentType: options.contentType,
579
645
  Metadata: options.metadata,
580
646
  CacheControl: options.cacheControl,
581
- ContentDisposition: options.contentDisposition
582
- }));
647
+ ContentDisposition: options.contentDisposition,
648
+ IfNoneMatch: options.ifNoneMatch
649
+ };
650
+ logOperation("write", "file.put", { key, fullPath, byteLength: buffered ? size : void 0, options });
651
+ if (buffered) {
652
+ await s3Client.send(new PutObjectCommand({ ...params, Body: body }), { abortSignal: options.signal });
653
+ } else {
654
+ async function* counted() {
655
+ for await (const chunk of toByteStream(data, options.signal)) {
656
+ size += chunk.byteLength;
657
+ yield chunk;
658
+ }
659
+ }
660
+ const stream = Readable.from(counted(), { objectMode: false });
661
+ const controller = new AbortController();
662
+ const abort = () => controller.abort();
663
+ options.signal?.addEventListener("abort", abort, { once: true });
664
+ try {
665
+ if (options.signal?.aborted)
666
+ abort();
667
+ const { Upload } = await import('@aws-sdk/lib-storage');
668
+ await new Upload({ client: s3Client, params: { ...params, Body: stream }, queueSize: 2, leavePartsOnError: false, abortController: controller }).done();
669
+ } finally {
670
+ options.signal?.removeEventListener("abort", abort);
671
+ stream.destroy();
672
+ }
673
+ }
583
674
  const headResult = await file.head(key);
584
675
  if (headResult.success) {
585
676
  return headResult;
586
677
  }
587
678
  return ok({
588
679
  key,
589
- size: body.length,
680
+ size,
590
681
  contentType: options.contentType || "application/octet-stream",
591
682
  lastModified: /* @__PURE__ */ new Date()
592
683
  });
@@ -595,11 +686,24 @@ function createS3Provider() {
595
686
  }
596
687
  },
597
688
  async get(key, options = {}) {
689
+ const streamed = await file.getStream(key, options);
690
+ if (!streamed.success)
691
+ return streamed;
692
+ try {
693
+ const chunks = [];
694
+ for await (const chunk of streamed.data)
695
+ chunks.push(chunk);
696
+ return ok(Buffer.concat(chunks));
697
+ } catch (error) {
698
+ return err(toStorageError2(error, key));
699
+ }
700
+ },
701
+ async getStream(key, options = {}) {
598
702
  try {
599
703
  const s3Client = getClient();
600
704
  const s3Config = getConfig();
601
705
  const fullPath = fullKey(key);
602
- logOperation("read", "file.get", { key, fullPath, options });
706
+ logOperation("read", "file.getStream", { key, fullPath, options });
603
707
  let Range;
604
708
  if (options.rangeStart !== void 0 || options.rangeEnd !== void 0) {
605
709
  const start = options.rangeStart ?? 0;
@@ -610,19 +714,15 @@ function createS3Provider() {
610
714
  Bucket: s3Config.bucket,
611
715
  Key: fullPath,
612
716
  Range
613
- }));
717
+ }), { abortSignal: options.signal });
614
718
  if (!response.Body) {
615
719
  return err(
616
720
  HaiStorageError.OPERATION_FAILED,
617
721
  storageM("storage_responseBodyEmpty")
618
722
  );
619
723
  }
620
- const chunks = [];
621
724
  const stream = response.Body;
622
- for await (const chunk of stream) {
623
- chunks.push(chunk);
624
- }
625
- return ok(Buffer.concat(chunks));
725
+ return ok(stream);
626
726
  } catch (error) {
627
727
  return err(toStorageError2(error, key));
628
728
  }
@@ -860,6 +960,9 @@ function createS3Provider() {
860
960
  region: cfg.region,
861
961
  endpoint: cfg.endpoint,
862
962
  forcePathStyle: cfg.forcePathStyle,
963
+ // 不强制兼容服务实现 AWS checksum 扩展;校验工具按实际字节独立计算。
964
+ requestChecksumCalculation: "WHEN_REQUIRED",
965
+ responseChecksumValidation: "WHEN_REQUIRED",
863
966
  credentials: {
864
967
  accessKeyId: cfg.accessKeyId,
865
968
  secretAccessKey: cfg.secretAccessKey
@@ -891,6 +994,37 @@ function createS3Provider() {
891
994
  }
892
995
  };
893
996
  }
997
+ var HEX_CHECKSUM = /^[\da-f]+$/i;
998
+ var BASE64_CHECKSUM = /^(?:[A-Z\d+/]{4})*(?:[A-Z\d+/]{2}==|[A-Z\d+/]{3}=)?$/i;
999
+ var ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512", "sha1", "md5"]);
1000
+ async function generate(data, options = {}) {
1001
+ try {
1002
+ const algorithm = options.algorithm ?? "sha256";
1003
+ if (!ALGORITHMS.has(algorithm))
1004
+ return err(HaiStorageError.INVALID_CHECKSUM, storageM("storage_checksumInvalid"));
1005
+ const hash = createHash(algorithm);
1006
+ for await (const chunk of toByteStream(data, options.signal))
1007
+ hash.update(chunk);
1008
+ return ok(hash.digest(options.encoding ?? "hex"));
1009
+ } catch (error) {
1010
+ return err(HaiStorageError.CHECKSUM_FAILED, storageM("storage_checksumFailed"), error);
1011
+ }
1012
+ }
1013
+ var checksum = {
1014
+ generate,
1015
+ async verify(data, expected, options = {}) {
1016
+ const encoding = options.encoding ?? "hex";
1017
+ const valid = encoding === "hex" ? expected.length % 2 === 0 && HEX_CHECKSUM.test(expected) : BASE64_CHECKSUM.test(expected) && expected.length > 0;
1018
+ if (!valid)
1019
+ return err(HaiStorageError.INVALID_CHECKSUM, storageM("storage_checksumInvalid"));
1020
+ const calculated = await generate(data, options);
1021
+ if (!calculated.success)
1022
+ return calculated;
1023
+ const actualBytes = Buffer.from(calculated.data, encoding);
1024
+ const expectedBytes = Buffer.from(expected, encoding);
1025
+ return ok(actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes));
1026
+ }
1027
+ };
894
1028
 
895
1029
  // src/storage-main.ts
896
1030
  var logger3 = core.logger.child({ module: "storage", scope: "main" });
@@ -921,6 +1055,7 @@ var notInitializedPresign = new Proxy(
921
1055
  }
922
1056
  );
923
1057
  var storage = {
1058
+ checksum,
924
1059
  /**
925
1060
  * 初始化存储连接。
926
1061
  *