@kumix/storage 0.1.0 → 0.1.2

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/s3.js CHANGED
@@ -1,12 +1,30 @@
1
- import { s as loadS3Config } from "./config-t-NVJICl.js";
1
+ import { s as loadS3Config } from "./config-By5ib98s.js";
2
2
  import { buildPublicUrl, getMimeType } from "./helpers.js";
3
- import { CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
5
- //#region src/operations/file-operations.ts
3
+ //#region src/operations/s3-sdk.ts
4
+ let s3SdkPromise;
5
+ let presignerPromise;
6
6
  /**
7
- * S3 file operations implementation
8
- * Provides comprehensive file management operations for S3-compatible storage providers
7
+ * Dynamically import `@aws-sdk/client-s3`, throwing a clear error when the
8
+ * optional peer dependency is not installed.
9
9
  */
10
+ async function loadS3Sdk() {
11
+ if (!s3SdkPromise) s3SdkPromise = import("@aws-sdk/client-s3").catch(() => {
12
+ throw new Error("@aws-sdk/client-s3 is not available. Install it to use the S3 storage provider.");
13
+ });
14
+ return s3SdkPromise;
15
+ }
16
+ /**
17
+ * Dynamically import `@aws-sdk/s3-request-presigner`, throwing a clear error
18
+ * when the optional peer dependency is not installed.
19
+ */
20
+ async function loadPresigner() {
21
+ if (!presignerPromise) presignerPromise = import("@aws-sdk/s3-request-presigner").catch(() => {
22
+ throw new Error("@aws-sdk/s3-request-presigner is not available. Install it to generate presigned URLs.");
23
+ });
24
+ return presignerPromise;
25
+ }
26
+ //#endregion
27
+ //#region src/operations/file-operations.ts
10
28
  /**
11
29
  * S3 file operations implementation
12
30
  * Handles all file-related operations for S3-compatible storage providers
@@ -21,6 +39,7 @@ var FileOperations = class {
21
39
  }
22
40
  async upload(options) {
23
41
  try {
42
+ const { PutObjectCommand } = await loadS3Sdk();
24
43
  const contentType = options.contentType || getMimeType(options.key);
25
44
  const command = new PutObjectCommand({
26
45
  Bucket: this.config.bucket,
@@ -30,7 +49,7 @@ var FileOperations = class {
30
49
  Metadata: options.metadata,
31
50
  CacheControl: options.cacheControl,
32
51
  ContentDisposition: options.contentDisposition,
33
- ACL: options.acl,
52
+ ...options.acl && (this.config.provider === "aws" || this.config.provider === "digitalocean") ? { ACL: options.acl } : {},
34
53
  Expires: options.expires
35
54
  });
36
55
  const result = await this.client.send(command);
@@ -52,6 +71,7 @@ var FileOperations = class {
52
71
  }
53
72
  async download(options) {
54
73
  try {
74
+ const { GetObjectCommand } = await loadS3Sdk();
55
75
  const command = new GetObjectCommand({
56
76
  Bucket: this.config.bucket,
57
77
  Key: options.key,
@@ -89,6 +109,7 @@ var FileOperations = class {
89
109
  }
90
110
  async delete(options) {
91
111
  try {
112
+ const { DeleteObjectCommand } = await loadS3Sdk();
92
113
  const command = new DeleteObjectCommand({
93
114
  Bucket: this.config.bucket,
94
115
  Key: options.key
@@ -104,6 +125,7 @@ var FileOperations = class {
104
125
  }
105
126
  async batchDelete(options) {
106
127
  try {
128
+ const { DeleteObjectsCommand } = await loadS3Sdk();
107
129
  const command = new DeleteObjectsCommand({
108
130
  Bucket: this.config.bucket,
109
131
  Delete: {
@@ -118,7 +140,7 @@ var FileOperations = class {
118
140
  error: err.Message || "Unknown error"
119
141
  })) || [];
120
142
  return {
121
- success: true,
143
+ success: errors.length === 0,
122
144
  deleted,
123
145
  errors: errors.length > 0 ? errors : void 0
124
146
  };
@@ -134,6 +156,7 @@ var FileOperations = class {
134
156
  }
135
157
  async list(options = {}) {
136
158
  try {
159
+ const { ListObjectsV2Command } = await loadS3Sdk();
137
160
  const command = new ListObjectsV2Command({
138
161
  Bucket: this.config.bucket,
139
162
  Prefix: options.prefix,
@@ -164,6 +187,7 @@ var FileOperations = class {
164
187
  }
165
188
  async exists(key) {
166
189
  try {
190
+ const { HeadObjectCommand } = await loadS3Sdk();
167
191
  const command = new HeadObjectCommand({
168
192
  Bucket: this.config.bucket,
169
193
  Key: key
@@ -190,9 +214,10 @@ var FileOperations = class {
190
214
  }
191
215
  async copy(options) {
192
216
  try {
217
+ const { CopyObjectCommand } = await loadS3Sdk();
193
218
  const command = new CopyObjectCommand({
194
219
  Bucket: this.config.bucket,
195
- CopySource: `${this.config.bucket}/${options.sourceKey}`,
220
+ CopySource: `${this.config.bucket}/${options.sourceKey.split("/").map(encodeURIComponent).join("/")}`,
196
221
  Key: options.destinationKey,
197
222
  Metadata: options.metadata,
198
223
  MetadataDirective: options.metadataDirective || "COPY"
@@ -241,6 +266,8 @@ var FileOperations = class {
241
266
  }
242
267
  async getPresignedUrl(options) {
243
268
  try {
269
+ const { GetObjectCommand, PutObjectCommand } = await loadS3Sdk();
270
+ const { getSignedUrl } = await loadPresigner();
244
271
  const expiresIn = options.expiresIn || 3600;
245
272
  let command;
246
273
  if (options.operation === "get") command = new GetObjectCommand({
@@ -269,15 +296,53 @@ var FileOperations = class {
269
296
  const isSupabase = this.config.provider === "supabase";
270
297
  if (this.config.publicUrl) return buildPublicUrl(this.config.publicUrl, isCloudflare ? "" : this.config.bucket, key, isSupabase);
271
298
  if (this.config.endpoint) return buildPublicUrl(this.config.endpoint, this.config.bucket, key, isSupabase);
272
- return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${key}`;
299
+ if (this.config.provider === "minio") throw new Error("MinIO public URL could not be built: set `publicUrl` (or `endpoint`) on the S3 config to a publicly reachable base, since MinIO has no default public host.");
300
+ const encodedKey = key.split("/").map(encodeURIComponent).join("/");
301
+ return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${encodedKey}`;
273
302
  }
274
303
  };
275
304
  //#endregion
276
305
  //#region src/operations/folder-operations.ts
306
+ /** S3 DeleteObjectsCommand allows at most 1000 keys per request */
307
+ const S3_DELETE_MAX_KEYS = 1e3;
277
308
  /**
278
- * S3 folder operations implementation
279
- * Provides comprehensive folder management operations for S3-compatible storage providers
309
+ * List ALL objects under a prefix, following pagination (handles >1000 keys)
280
310
  */
311
+ async function listAllObjects(client, bucket, prefix) {
312
+ const { ListObjectsV2Command } = await loadS3Sdk();
313
+ const objects = [];
314
+ let continuationToken;
315
+ do {
316
+ const command = new ListObjectsV2Command({
317
+ Bucket: bucket,
318
+ Prefix: prefix,
319
+ ContinuationToken: continuationToken
320
+ });
321
+ const result = await client.send(command);
322
+ if (result.Contents) objects.push(...result.Contents);
323
+ continuationToken = result.IsTruncated ? result.NextContinuationToken : void 0;
324
+ } while (continuationToken);
325
+ return objects;
326
+ }
327
+ /** Delete objects in batches of 1000 (S3 hard limit per DeleteObjectsCommand) */
328
+ async function deleteObjectsBatched(client, bucket, keys) {
329
+ const { DeleteObjectsCommand } = await loadS3Sdk();
330
+ const deleted = [];
331
+ for (let i = 0; i < keys.length; i += S3_DELETE_MAX_KEYS) {
332
+ const command = new DeleteObjectsCommand({
333
+ Bucket: bucket,
334
+ Delete: {
335
+ Objects: keys.slice(i, i + S3_DELETE_MAX_KEYS).map((key) => ({ Key: key })),
336
+ Quiet: true
337
+ }
338
+ });
339
+ const result = await client.send(command);
340
+ if (result.Deleted) {
341
+ for (const obj of result.Deleted) if (obj.Key) deleted.push(obj.Key);
342
+ }
343
+ }
344
+ return deleted;
345
+ }
281
346
  /**
282
347
  * S3 folder operations implementation
283
348
  * Handles all folder-related operations for S3-compatible storage providers
@@ -292,6 +357,7 @@ var FolderOperations = class {
292
357
  }
293
358
  async createFolder(options) {
294
359
  try {
360
+ const { PutObjectCommand } = await loadS3Sdk();
295
361
  const folderPath = options.path.endsWith("/") ? options.path : `${options.path}/`;
296
362
  const command = new PutObjectCommand({
297
363
  Bucket: this.config.bucket,
@@ -313,27 +379,18 @@ var FolderOperations = class {
313
379
  }
314
380
  async deleteFolder(options) {
315
381
  try {
382
+ const { DeleteObjectsCommand } = await loadS3Sdk();
316
383
  const folderPath = options.path.endsWith("/") ? options.path : `${options.path}/`;
317
384
  if (options.recursive) {
318
- const listCommand = new ListObjectsV2Command({
319
- Bucket: this.config.bucket,
320
- Prefix: folderPath
321
- });
322
- const listResult = await this.client.send(listCommand);
323
- if (!listResult.Contents || listResult.Contents.length === 0) return {
385
+ const allObjects = await listAllObjects(this.client, this.config.bucket, folderPath);
386
+ if (allObjects.length === 0) return {
324
387
  success: true,
325
388
  deletedFiles: []
326
389
  };
327
- const deleteCommand = new DeleteObjectsCommand({
328
- Bucket: this.config.bucket,
329
- Delete: {
330
- Objects: listResult.Contents.map((obj) => ({ Key: obj.Key })),
331
- Quiet: false
332
- }
333
- });
390
+ const keys = allObjects.map((obj) => obj.Key).filter(Boolean);
334
391
  return {
335
392
  success: true,
336
- deletedFiles: (await this.client.send(deleteCommand)).Deleted?.map((obj) => obj.Key).filter(Boolean) || []
393
+ deletedFiles: await deleteObjectsBatched(this.client, this.config.bucket, keys)
337
394
  };
338
395
  } else {
339
396
  const deleteCommand = new DeleteObjectsCommand({
@@ -358,6 +415,7 @@ var FolderOperations = class {
358
415
  }
359
416
  async listFolders(options = {}) {
360
417
  try {
418
+ const { ListObjectsV2Command } = await loadS3Sdk();
361
419
  const command = new ListObjectsV2Command({
362
420
  Bucket: this.config.bucket,
363
421
  Prefix: options.prefix,
@@ -372,10 +430,7 @@ var FolderOperations = class {
372
430
  const path = cp.Prefix;
373
431
  return {
374
432
  name: path.split("/").filter(Boolean).pop() || "",
375
- path,
376
- size: 0,
377
- fileCount: 0,
378
- lastModified: /* @__PURE__ */ new Date()
433
+ path
379
434
  };
380
435
  }) || [],
381
436
  files: result.Contents?.map((obj) => ({
@@ -397,6 +452,7 @@ var FolderOperations = class {
397
452
  }
398
453
  async folderExists(path) {
399
454
  try {
455
+ const { ListObjectsV2Command } = await loadS3Sdk();
400
456
  const folderPath = path.endsWith("/") ? path : `${path}/`;
401
457
  const command = new ListObjectsV2Command({
402
458
  Bucket: this.config.bucket,
@@ -408,10 +464,7 @@ var FolderOperations = class {
408
464
  exists: true,
409
465
  folderInfo: {
410
466
  name: folderPath.split("/").filter(Boolean).pop() || "",
411
- path: folderPath,
412
- size: 0,
413
- fileCount: 0,
414
- lastModified: /* @__PURE__ */ new Date()
467
+ path: folderPath
415
468
  }
416
469
  };
417
470
  return { exists: false };
@@ -424,37 +477,28 @@ var FolderOperations = class {
424
477
  }
425
478
  async renameFolder(options) {
426
479
  try {
480
+ const { CopyObjectCommand } = await loadS3Sdk();
427
481
  const oldPath = options.oldPath.endsWith("/") ? options.oldPath : `${options.oldPath}/`;
428
482
  const newPath = options.newPath.endsWith("/") ? options.newPath : `${options.newPath}/`;
429
- const listCommand = new ListObjectsV2Command({
430
- Bucket: this.config.bucket,
431
- Prefix: oldPath
432
- });
433
- const listResult = await this.client.send(listCommand);
434
- if (!listResult.Contents || listResult.Contents.length === 0) return {
483
+ const allObjects = await listAllObjects(this.client, this.config.bucket, oldPath);
484
+ if (allObjects.length === 0) return {
435
485
  success: true,
436
486
  movedFiles: []
437
487
  };
438
488
  const movedFiles = [];
439
- for (const obj of listResult.Contents) {
489
+ for (const obj of allObjects) {
440
490
  const oldKey = obj.Key;
441
- const newKey = oldKey.replace(oldPath, newPath);
491
+ const newKey = newPath + oldKey.slice(oldPath.length);
442
492
  const copyCommand = new CopyObjectCommand({
443
493
  Bucket: this.config.bucket,
444
494
  Key: newKey,
445
- CopySource: `${this.config.bucket}/${oldKey}`
495
+ CopySource: `${this.config.bucket}/${oldKey.split("/").map(encodeURIComponent).join("/")}`
446
496
  });
447
497
  await this.client.send(copyCommand);
448
498
  movedFiles.push(newKey);
449
499
  }
450
- const deleteCommand = new DeleteObjectsCommand({
451
- Bucket: this.config.bucket,
452
- Delete: {
453
- Objects: listResult.Contents.map((obj) => ({ Key: obj.Key })),
454
- Quiet: true
455
- }
456
- });
457
- await this.client.send(deleteCommand);
500
+ const oldKeys = allObjects.map((obj) => obj.Key).filter(Boolean);
501
+ await deleteObjectsBatched(this.client, this.config.bucket, oldKeys);
458
502
  return {
459
503
  success: true,
460
504
  movedFiles
@@ -468,25 +512,26 @@ var FolderOperations = class {
468
512
  }
469
513
  async copyFolder(options) {
470
514
  try {
515
+ const { CopyObjectCommand } = await loadS3Sdk();
516
+ const recursive = options.recursive ?? true;
471
517
  const sourcePath = options.sourcePath.endsWith("/") ? options.sourcePath : `${options.sourcePath}/`;
472
518
  const destPath = options.destinationPath.endsWith("/") ? options.destinationPath : `${options.destinationPath}/`;
473
- const listCommand = new ListObjectsV2Command({
474
- Bucket: this.config.bucket,
475
- Prefix: sourcePath
476
- });
477
- const listResult = await this.client.send(listCommand);
478
- if (!listResult.Contents || listResult.Contents.length === 0) return {
519
+ const allObjects = await listAllObjects(this.client, this.config.bucket, sourcePath);
520
+ if (allObjects.length === 0) return {
479
521
  success: true,
480
522
  copiedFiles: []
481
523
  };
482
524
  const copiedFiles = [];
483
- for (const obj of listResult.Contents) {
525
+ for (const obj of allObjects) {
484
526
  const sourceKey = obj.Key;
485
- const destKey = sourceKey.replace(sourcePath, destPath);
527
+ if (!recursive) {
528
+ if (sourceKey.slice(sourcePath.length).includes("/")) continue;
529
+ }
530
+ const destKey = destPath + sourceKey.slice(sourcePath.length);
486
531
  const copyCommand = new CopyObjectCommand({
487
532
  Bucket: this.config.bucket,
488
533
  Key: destKey,
489
- CopySource: `${this.config.bucket}/${sourceKey}`
534
+ CopySource: `${this.config.bucket}/${sourceKey.split("/").map(encodeURIComponent).join("/")}`
490
535
  });
491
536
  await this.client.send(copyCommand);
492
537
  copiedFiles.push(destKey);
@@ -506,83 +551,105 @@ var FolderOperations = class {
506
551
  //#endregion
507
552
  //#region src/providers/s3.ts
508
553
  /**
509
- * S3-compatible storage provider implementation
510
- * Provides cloud storage operations using AWS S3 SDK for S3-compatible services
511
- */
512
- /**
513
554
  * S3-compatible storage provider
514
555
  * Implements StorageInterface for AWS S3 and S3-compatible services
515
556
  * @internal
516
557
  */
517
558
  var S3Provider = class {
518
- client;
519
559
  config;
520
- fileOps;
521
- folderOps;
560
+ _client = null;
561
+ _fileOps = null;
562
+ _folderOps = null;
522
563
  constructor(config) {
523
564
  this.config = config;
524
- this.client = new S3Client({
525
- region: config.region,
526
- credentials: {
527
- accessKeyId: config.accessKeyId,
528
- secretAccessKey: config.secretAccessKey
529
- },
530
- endpoint: config.endpoint,
531
- forcePathStyle: config.forcePathStyle || false
532
- });
533
- this.fileOps = new FileOperations(this.client, this.config);
534
- this.folderOps = new FolderOperations(this.client, this.config);
565
+ }
566
+ /**
567
+ * Lazily construct the S3 client and operation helpers. The `@aws-sdk/client-s3`
568
+ * dependency is imported dynamically so the optional peer isn't required at
569
+ * module load — consumers using only Cloudinary can import the package without
570
+ * installing it.
571
+ */
572
+ async getFileOps() {
573
+ if (!this._fileOps) {
574
+ const client = await this.getClient();
575
+ this._fileOps = new FileOperations(client, this.config);
576
+ }
577
+ return this._fileOps;
578
+ }
579
+ async getFolderOps() {
580
+ if (!this._folderOps) {
581
+ const client = await this.getClient();
582
+ this._folderOps = new FolderOperations(client, this.config);
583
+ }
584
+ return this._folderOps;
585
+ }
586
+ async getClient() {
587
+ if (!this._client) {
588
+ const { S3Client } = await loadS3Sdk();
589
+ this._client = new S3Client({
590
+ region: this.config.region,
591
+ credentials: {
592
+ accessKeyId: this.config.accessKeyId,
593
+ secretAccessKey: this.config.secretAccessKey,
594
+ ...this.config.sessionToken ? { sessionToken: this.config.sessionToken } : {}
595
+ },
596
+ endpoint: this.config.endpoint,
597
+ forcePathStyle: this.config.forcePathStyle ?? false
598
+ });
599
+ }
600
+ return this._client;
535
601
  }
536
602
  async upload(options) {
537
- return this.fileOps.upload(options);
603
+ return (await this.getFileOps()).upload(options);
538
604
  }
539
605
  async download(options) {
540
- return this.fileOps.download(options);
606
+ return (await this.getFileOps()).download(options);
541
607
  }
542
608
  async delete(options) {
543
- return this.fileOps.delete(options);
609
+ return (await this.getFileOps()).delete(options);
544
610
  }
545
611
  async batchDelete(options) {
546
- return this.fileOps.batchDelete(options);
612
+ return (await this.getFileOps()).batchDelete(options);
547
613
  }
548
614
  async list(options) {
549
- return this.fileOps.list(options);
615
+ return (await this.getFileOps()).list(options);
550
616
  }
551
617
  async exists(key) {
552
- return this.fileOps.exists(key);
618
+ return (await this.getFileOps()).exists(key);
553
619
  }
554
620
  async copy(options) {
555
- return this.fileOps.copy(options);
621
+ return (await this.getFileOps()).copy(options);
556
622
  }
557
623
  async move(options) {
558
- return this.fileOps.move(options);
624
+ return (await this.getFileOps()).move(options);
559
625
  }
560
626
  async duplicate(options) {
561
- return this.fileOps.duplicate(options);
627
+ return (await this.getFileOps()).duplicate(options);
562
628
  }
563
629
  async getPresignedUrl(options) {
564
- return this.fileOps.getPresignedUrl(options);
630
+ return (await this.getFileOps()).getPresignedUrl(options);
565
631
  }
566
632
  getPublicUrl(key) {
567
- return this.fileOps.getPublicUrl(key);
633
+ if (this._fileOps) return this._fileOps.getPublicUrl(key);
634
+ return new FileOperations(null, this.config).getPublicUrl(key);
568
635
  }
569
636
  async createFolder(options) {
570
- return this.folderOps.createFolder(options);
637
+ return (await this.getFolderOps()).createFolder(options);
571
638
  }
572
639
  async deleteFolder(options) {
573
- return this.folderOps.deleteFolder(options);
640
+ return (await this.getFolderOps()).deleteFolder(options);
574
641
  }
575
642
  async listFolders(options) {
576
- return this.folderOps.listFolders(options);
643
+ return (await this.getFolderOps()).listFolders(options);
577
644
  }
578
645
  async folderExists(path) {
579
- return this.folderOps.folderExists(path);
646
+ return (await this.getFolderOps()).folderExists(path);
580
647
  }
581
648
  async renameFolder(options) {
582
- return this.folderOps.renameFolder(options);
649
+ return (await this.getFolderOps()).renameFolder(options);
583
650
  }
584
651
  async copyFolder(options) {
585
- return this.folderOps.copyFolder(options);
652
+ return (await this.getFolderOps()).copyFolder(options);
586
653
  }
587
654
  };
588
655
  //#endregion