@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/dist/node.js CHANGED
@@ -1,7 +1,7 @@
1
- import { HaiStorageError, StorageConfigSchema, PresignUploadOptionsSchema, PresignOptionsSchema } from './chunk-P4NA6BCH.js';
2
- export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-P4NA6BCH.js';
3
- import { storageM, MIME_TYPES, MIME_TYPE_DEFAULT } from './chunk-KVFEQHRX.js';
4
- export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-KVFEQHRX.js';
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
- return key.endsWith(".meta.json") || key.endsWith(".upload.tmp");
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
- return safePath(getConfig().root, key);
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 = `${filePath}.meta.json`;
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
- if (isMetaFile(sourceKey) || isMetaFile(destKey)) {
402
- return err(
403
- HaiStorageError.INVALID_PATH,
404
- storageM("storage_metaFileAccess", { params: { key: sourceKey } }),
405
- sourceKey
406
- );
407
- }
408
- try {
409
- const sourcePath = fullPath(sourceKey);
410
- const destPath = fullPath(destKey);
411
- logOperation("write", "file.copy", { sourceKey, destKey, options });
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
- presignClient = new S3Client({ ...clientConfig, endpoint: cfg.publicEndpoint });
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 }));