@h-ai/storage 1.0.0-beta.1 → 1.0.0-beta.11
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 +4 -2
- package/README.ja.md +4 -2
- package/README.md +4 -2
- package/dist/browser.d.ts +3 -2
- package/dist/browser.js +2 -2
- package/dist/{chunk-P4NA6BCH.js → chunk-ANAESRWS.js} +8 -6
- package/dist/chunk-ANAESRWS.js.map +1 -0
- package/dist/{chunk-KVFEQHRX.js → chunk-TLZY4IP6.js} +5 -5
- package/dist/chunk-TLZY4IP6.js.map +1 -0
- package/dist/client/index.js +1 -1
- package/dist/node.js +62 -54
- package/dist/node.js.map +1 -1
- package/package.json +6 -3
- package/dist/chunk-KVFEQHRX.js.map +0 -1
- package/dist/chunk-P4NA6BCH.js.map +0 -1
package/dist/node.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { HaiStorageError, StorageConfigSchema, PresignUploadOptionsSchema, PresignOptionsSchema } 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-
|
|
1
|
+
import { HaiStorageError, StorageConfigSchema, PresignUploadOptionsSchema, PresignOptionsSchema } from './chunk-ANAESRWS.js';
|
|
2
|
+
export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-ANAESRWS.js';
|
|
3
|
+
import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-TLZY4IP6.js';
|
|
4
|
+
export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-TLZY4IP6.js';
|
|
5
5
|
export { STORAGE_DELETE_MAX_KEYS, STORAGE_KEY_MAX_BYTES, StorageDeleteKeysSchema, StorageKeySchema, StoragePrefixSchema, StoragePresignExpiresInSchema, getStorageKeyIssue } from './chunk-P65WYXJG.js';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { core, err, ok } from '@h-ai/core';
|
|
@@ -167,7 +167,8 @@ function calculateEtag(stat2) {
|
|
|
167
167
|
return `"${stat2.size.toString(16)}-${stat2.mtimeMs.toString(16)}"`;
|
|
168
168
|
}
|
|
169
169
|
function isMetaFile(key) {
|
|
170
|
-
|
|
170
|
+
const normalized = key.toLowerCase();
|
|
171
|
+
return normalized.endsWith(".meta.json") || normalized.endsWith(".upload.tmp");
|
|
171
172
|
}
|
|
172
173
|
function createInvalidPathError() {
|
|
173
174
|
const error = new Error(storageM("storage_pathTraversal"));
|
|
@@ -182,7 +183,7 @@ function safePath(root, key) {
|
|
|
182
183
|
if (path.isAbsolute(key)) {
|
|
183
184
|
throw createInvalidPathError();
|
|
184
185
|
}
|
|
185
|
-
if (key.includes("\0") || hasPathTraversalSegment(key)) {
|
|
186
|
+
if (key.includes("\0") || hasPathTraversalSegment(key) || process.platform === "win32" && key.split(PATH_SEGMENT_SPLIT_REGEX).some((segment) => /[:. ]$/.test(segment) || segment.includes(":"))) {
|
|
186
187
|
throw createInvalidPathError();
|
|
187
188
|
}
|
|
188
189
|
const normalized = path.normalize(key);
|
|
@@ -203,8 +204,22 @@ function createLocalProvider() {
|
|
|
203
204
|
}
|
|
204
205
|
return config;
|
|
205
206
|
}
|
|
206
|
-
function fullPath(key) {
|
|
207
|
-
|
|
207
|
+
async function fullPath(key) {
|
|
208
|
+
const root = getConfig().root;
|
|
209
|
+
const target = safePath(root, key);
|
|
210
|
+
let current = root;
|
|
211
|
+
for (const segment of path.relative(root, target).split(path.sep).filter(Boolean)) {
|
|
212
|
+
current = path.join(current, segment);
|
|
213
|
+
try {
|
|
214
|
+
if ((await fsp.lstat(current)).isSymbolicLink())
|
|
215
|
+
throw createInvalidPathError();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error.code === "ENOENT")
|
|
218
|
+
break;
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return target;
|
|
208
223
|
}
|
|
209
224
|
function logOperation(category, operation, payload) {
|
|
210
225
|
logStorageOperation(logger, config?.operationLog, category, operation, payload);
|
|
@@ -219,7 +234,8 @@ function createLocalProvider() {
|
|
|
219
234
|
}
|
|
220
235
|
let temporaryPath;
|
|
221
236
|
try {
|
|
222
|
-
const filePath = fullPath(key);
|
|
237
|
+
const filePath = await fullPath(key);
|
|
238
|
+
const metaPath = await fullPath(`${key}.meta.json`);
|
|
223
239
|
logOperation("write", "file.put", { key, options });
|
|
224
240
|
const dir2 = path.dirname(filePath);
|
|
225
241
|
await fsp.mkdir(dir2, { recursive: true, mode: getConfig().directoryMode });
|
|
@@ -244,11 +260,12 @@ function createLocalProvider() {
|
|
|
244
260
|
metadata: options.metadata
|
|
245
261
|
};
|
|
246
262
|
if (options.metadata || options.contentType) {
|
|
247
|
-
const metaPath = `${filePath}.meta.json`;
|
|
248
263
|
await fsp.writeFile(metaPath, JSON.stringify({
|
|
249
264
|
contentType: options.contentType,
|
|
250
265
|
metadata: options.metadata
|
|
251
266
|
}));
|
|
267
|
+
} else {
|
|
268
|
+
await fsp.rm(metaPath, { force: true });
|
|
252
269
|
}
|
|
253
270
|
return ok(metadata);
|
|
254
271
|
} catch (error) {
|
|
@@ -281,7 +298,7 @@ function createLocalProvider() {
|
|
|
281
298
|
);
|
|
282
299
|
}
|
|
283
300
|
try {
|
|
284
|
-
const filePath = fullPath(key);
|
|
301
|
+
const filePath = await fullPath(key);
|
|
285
302
|
logOperation("read", "file.getStream", { key, options });
|
|
286
303
|
options.signal?.throwIfAborted();
|
|
287
304
|
const stat2 = await fsp.stat(filePath);
|
|
@@ -305,7 +322,7 @@ function createLocalProvider() {
|
|
|
305
322
|
);
|
|
306
323
|
}
|
|
307
324
|
try {
|
|
308
|
-
const filePath = fullPath(key);
|
|
325
|
+
const filePath = await fullPath(key);
|
|
309
326
|
logOperation("read", "file.head", { key });
|
|
310
327
|
const stat2 = await fsp.stat(filePath);
|
|
311
328
|
if (stat2.isDirectory()) {
|
|
@@ -316,7 +333,7 @@ function createLocalProvider() {
|
|
|
316
333
|
}
|
|
317
334
|
let customMetadata = {};
|
|
318
335
|
try {
|
|
319
|
-
const metaPath = `${
|
|
336
|
+
const metaPath = await fullPath(`${key}.meta.json`);
|
|
320
337
|
const metaContent = await fsp.readFile(metaPath, "utf-8");
|
|
321
338
|
customMetadata = JSON.parse(metaContent);
|
|
322
339
|
} catch {
|
|
@@ -342,7 +359,7 @@ function createLocalProvider() {
|
|
|
342
359
|
);
|
|
343
360
|
}
|
|
344
361
|
try {
|
|
345
|
-
const filePath = fullPath(key);
|
|
362
|
+
const filePath = await fullPath(key);
|
|
346
363
|
logOperation("read", "file.exists", { key });
|
|
347
364
|
await fsp.access(filePath, fs.constants.F_OK);
|
|
348
365
|
return ok(true);
|
|
@@ -361,7 +378,7 @@ function createLocalProvider() {
|
|
|
361
378
|
);
|
|
362
379
|
}
|
|
363
380
|
try {
|
|
364
|
-
const filePath = fullPath(key);
|
|
381
|
+
const filePath = await fullPath(key);
|
|
365
382
|
logOperation("write", "file.delete", { key });
|
|
366
383
|
logger.debug("Deleting file", { key });
|
|
367
384
|
await fsp.unlink(filePath);
|
|
@@ -398,40 +415,17 @@ function createLocalProvider() {
|
|
|
398
415
|
return toStorageDeleteResult(summary);
|
|
399
416
|
},
|
|
400
417
|
async copy(sourceKey, destKey, options = {}) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
logger.debug("Copying file", { sourceKey, destKey });
|
|
413
|
-
await fsp.mkdir(path.dirname(destPath), { recursive: true, mode: getConfig().directoryMode });
|
|
414
|
-
await fsp.copyFile(sourcePath, destPath);
|
|
415
|
-
const stat2 = await fsp.stat(destPath);
|
|
416
|
-
const metadata = {
|
|
417
|
-
key: destKey,
|
|
418
|
-
size: stat2.size,
|
|
419
|
-
contentType: options.contentType || getMimeType2(destKey),
|
|
420
|
-
lastModified: stat2.mtime,
|
|
421
|
-
etag: calculateEtag(stat2),
|
|
422
|
-
metadata: options.metadata
|
|
423
|
-
};
|
|
424
|
-
if (options.contentType || options.metadata) {
|
|
425
|
-
const metaPath = `${destPath}.meta.json`;
|
|
426
|
-
await fsp.writeFile(metaPath, JSON.stringify({
|
|
427
|
-
contentType: options.contentType,
|
|
428
|
-
metadata: options.metadata
|
|
429
|
-
}));
|
|
430
|
-
}
|
|
431
|
-
return ok(metadata);
|
|
432
|
-
} catch (error) {
|
|
433
|
-
return err(toStorageError(error, sourceKey));
|
|
434
|
-
}
|
|
418
|
+
logOperation("write", "file.copy", { sourceKey, destKey, options });
|
|
419
|
+
const source = await file.head(sourceKey);
|
|
420
|
+
if (!source.success)
|
|
421
|
+
return source;
|
|
422
|
+
const stream = await file.getStream(sourceKey);
|
|
423
|
+
if (!stream.success)
|
|
424
|
+
return stream;
|
|
425
|
+
return file.put(destKey, stream.data, {
|
|
426
|
+
contentType: options.contentType ?? source.data.contentType,
|
|
427
|
+
metadata: options.metadata ?? source.data.metadata
|
|
428
|
+
});
|
|
435
429
|
}
|
|
436
430
|
};
|
|
437
431
|
const dir = {
|
|
@@ -524,7 +518,7 @@ function createLocalProvider() {
|
|
|
524
518
|
},
|
|
525
519
|
async delete(prefix) {
|
|
526
520
|
try {
|
|
527
|
-
const dirPath = fullPath(prefix);
|
|
521
|
+
const dirPath = await fullPath(prefix);
|
|
528
522
|
logOperation("write", "dir.delete", { prefix });
|
|
529
523
|
await fsp.rm(dirPath, { recursive: true, force: true });
|
|
530
524
|
return ok(void 0);
|
|
@@ -566,6 +560,7 @@ function createLocalProvider() {
|
|
|
566
560
|
config = cfg;
|
|
567
561
|
logger.info("Connecting local provider", { root: cfg.root });
|
|
568
562
|
await fsp.mkdir(cfg.root, { recursive: true, mode: cfg.directoryMode });
|
|
563
|
+
config = { ...cfg, root: await fsp.realpath(cfg.root) };
|
|
569
564
|
connected = true;
|
|
570
565
|
logger.info("Local provider connected", { root: cfg.root });
|
|
571
566
|
return ok(void 0);
|
|
@@ -646,12 +641,14 @@ function createS3Provider() {
|
|
|
646
641
|
let client = null;
|
|
647
642
|
let deleteClient = null;
|
|
648
643
|
let presignClient = null;
|
|
644
|
+
let publicBasePath = "";
|
|
649
645
|
let config = null;
|
|
650
646
|
let ready = false;
|
|
651
647
|
let closing = null;
|
|
652
648
|
function closeClients() {
|
|
653
649
|
ready = false;
|
|
654
650
|
config = null;
|
|
651
|
+
publicBasePath = "";
|
|
655
652
|
if (closing)
|
|
656
653
|
return closing;
|
|
657
654
|
closing = Promise.resolve().then(() => {
|
|
@@ -685,6 +682,12 @@ function createS3Provider() {
|
|
|
685
682
|
}
|
|
686
683
|
return selected;
|
|
687
684
|
}
|
|
685
|
+
function publicPresignedUrl(url, basePath) {
|
|
686
|
+
if (!basePath)
|
|
687
|
+
return url;
|
|
688
|
+
const pathOffset = url.indexOf("/", url.indexOf("://") + 3);
|
|
689
|
+
return `${url.slice(0, pathOffset)}${basePath}${url.slice(pathOffset)}`;
|
|
690
|
+
}
|
|
688
691
|
function getConfig() {
|
|
689
692
|
if (!ready || !config) {
|
|
690
693
|
throw new Error(storageM("storage_s3ConfigNotInitialized"));
|
|
@@ -1002,6 +1005,7 @@ function createS3Provider() {
|
|
|
1002
1005
|
const s3Config = getConfig();
|
|
1003
1006
|
const fullPath = fullKey(key);
|
|
1004
1007
|
logOperation("read", "presign.getUrl", { key, fullPath, options });
|
|
1008
|
+
const basePath = publicBasePath;
|
|
1005
1009
|
const command = new GetObjectCommand({
|
|
1006
1010
|
Bucket: s3Config.bucket,
|
|
1007
1011
|
Key: fullPath,
|
|
@@ -1011,7 +1015,7 @@ function createS3Provider() {
|
|
|
1011
1015
|
const url = await getSignedUrl(s3Client, command, {
|
|
1012
1016
|
expiresIn: parsed.expiresIn
|
|
1013
1017
|
});
|
|
1014
|
-
return ok(url);
|
|
1018
|
+
return ok(publicPresignedUrl(url, basePath));
|
|
1015
1019
|
} catch (error) {
|
|
1016
1020
|
return err(
|
|
1017
1021
|
HaiStorageError.PRESIGN_FAILED,
|
|
@@ -1027,6 +1031,7 @@ function createS3Provider() {
|
|
|
1027
1031
|
const s3Config = getConfig();
|
|
1028
1032
|
const fullPath = fullKey(key);
|
|
1029
1033
|
logOperation("write", "presign.putUrl", { key, fullPath, options });
|
|
1034
|
+
const basePath = publicBasePath;
|
|
1030
1035
|
const command = new PutObjectCommand({
|
|
1031
1036
|
Bucket: s3Config.bucket,
|
|
1032
1037
|
Key: fullPath,
|
|
@@ -1035,7 +1040,7 @@ function createS3Provider() {
|
|
|
1035
1040
|
const url = await getSignedUrl(s3Client, command, {
|
|
1036
1041
|
expiresIn: parsed.expiresIn
|
|
1037
1042
|
});
|
|
1038
|
-
return ok(url);
|
|
1043
|
+
return ok(publicPresignedUrl(url, basePath));
|
|
1039
1044
|
} catch (error) {
|
|
1040
1045
|
return err(
|
|
1041
1046
|
HaiStorageError.PRESIGN_FAILED,
|
|
@@ -1088,8 +1093,11 @@ function createS3Provider() {
|
|
|
1088
1093
|
};
|
|
1089
1094
|
client = new S3Client(clientConfig);
|
|
1090
1095
|
deleteClient = new S3Client({ ...clientConfig, maxAttempts: 1 });
|
|
1091
|
-
if (cfg.publicEndpoint)
|
|
1092
|
-
|
|
1096
|
+
if (cfg.publicEndpoint) {
|
|
1097
|
+
const endpoint = new URL(cfg.publicEndpoint);
|
|
1098
|
+
publicBasePath = endpoint.pathname.replace(TRAILING_SLASHES_REGEX, "");
|
|
1099
|
+
presignClient = new S3Client({ ...clientConfig, endpoint: endpoint.origin, forcePathStyle: publicBasePath ? true : cfg.forcePathStyle });
|
|
1100
|
+
}
|
|
1093
1101
|
if (cfg.createBucket) {
|
|
1094
1102
|
try {
|
|
1095
1103
|
await client.send(new HeadBucketCommand({ Bucket: cfg.bucket }));
|