@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/README.en.md +11 -1
- package/README.ja.md +11 -1
- package/README.md +11 -1
- package/dist/browser.d.ts +34 -4
- package/dist/browser.js +2 -2
- package/dist/{chunk-U427BQIQ.js → chunk-25GYTRA7.js} +12 -4
- package/dist/chunk-25GYTRA7.js.map +1 -0
- package/dist/{chunk-W44BX2GI.js → chunk-KXE6OO5T.js} +6 -4
- package/dist/chunk-KXE6OO5T.js.map +1 -0
- package/dist/client/index.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +188 -53
- package/dist/node.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-U427BQIQ.js.map +0 -1
- package/dist/chunk-W44BX2GI.js.map +0 -1
package/dist/node.js
CHANGED
|
@@ -1,16 +1,63 @@
|
|
|
1
|
-
import { HaiStorageError, StorageConfigSchema } from './chunk-
|
|
2
|
-
export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-
|
|
3
|
-
import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-
|
|
4
|
-
export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-
|
|
5
|
-
import { core,
|
|
6
|
-
import { Buffer } from 'buffer';
|
|
7
|
-
import * as crypto from 'crypto';
|
|
8
|
-
import
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
574
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
*
|