lean-s3 0.4.1 → 0.6.0

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.md CHANGED
@@ -137,6 +137,8 @@ See [DESIGN_DECISIONS.md](./DESIGN_DECISIONS.md) to read about why this library
137
137
  - ✅ [`PutObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) via `S3File.write`
138
138
  - ✅ [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) via `S3File.exists`/`S3File.stat`
139
139
  - ✅ [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html) via `.listMultipartUploads`
140
+ - ✅ [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html) via `.listParts`
141
+ - ✅ [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html) via `.uploadPart`
140
142
  - ✅ [`CompleteMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html) via `.completeMultipartUpload`
141
143
  - ✅ [`AbortMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html) via `.abortMultipartUpload`
142
144
 
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ interface S3FilePresignOptions {
41
41
  /** Seconds. */
42
42
  expiresIn: number;
43
43
  method: PresignableHttpMethod;
44
+ contentLength: number;
44
45
  storageClass: StorageClass;
45
46
  acl: Acl;
46
47
  }
@@ -99,7 +100,7 @@ type CreateMultipartUploadOptions = {
99
100
  bucket?: string;
100
101
  signal?: AbortSignal;
101
102
  };
102
- type CreateMultipartUploadResponse = {
103
+ type CreateMultipartUploadResult = {
103
104
  bucket: string;
104
105
  key: string;
105
106
  uploadId: string;
@@ -128,7 +129,44 @@ type MultipartUploadPart = {
128
129
  partNumber: number;
129
130
  etag: string;
130
131
  };
131
- type ListObjectsResponse = {
132
+ type UploadPartOptions = {
133
+ bucket?: string;
134
+ signal?: AbortSignal;
135
+ };
136
+ type UploadPartResult = {
137
+ partNumber: number;
138
+ etag: string;
139
+ };
140
+ type ListPartsOptions = {
141
+ maxParts?: number;
142
+ partNumberMarker?: string;
143
+ bucket?: string;
144
+ signal?: AbortSignal;
145
+ };
146
+ type ListPartsResult = {
147
+ bucket: string;
148
+ key: string;
149
+ uploadId: string;
150
+ partNumberMarker?: string;
151
+ nextPartNumberMarker?: string;
152
+ maxParts?: number;
153
+ isTruncated: boolean;
154
+ parts: Array<{
155
+ checksumCRC32?: string;
156
+ checksumCRC32C?: string;
157
+ checksumCRC64NVME?: string;
158
+ checksumSHA1?: string;
159
+ checksumSHA256?: string;
160
+ etag: string;
161
+ lastModified: Date;
162
+ partNumber: number;
163
+ size: number;
164
+ }>;
165
+ storageClass?: StorageClass;
166
+ checksumAlgorithm?: ChecksumAlgorithm;
167
+ checksumType?: ChecksumType;
168
+ };
169
+ type ListObjectsResult = {
132
170
  name: string;
133
171
  prefix: string | undefined;
134
172
  startAfter: string | undefined;
@@ -224,8 +262,8 @@ declare class S3Client {
224
262
  * ```
225
263
  */
226
264
  presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
227
- storageClass, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
228
- createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResponse>;
265
+ storageClass, contentLength, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
266
+ createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
229
267
  /**
230
268
  * @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
231
269
  * @throws {RangeError} If `options.maxKeys` is not between `1` and `1000`.
@@ -242,18 +280,22 @@ declare class S3Client {
242
280
  * @throws {RangeError} If `key` is not at least 1 character long.
243
281
  * @throws {Error} If `uploadId` is not provided.
244
282
  */
245
- completeMultipartUpload(key: string, uploadId: string, parts: readonly MultipartUploadPart[], options?: CompleteMultipartUploadOptions): Promise<{
246
- location: any;
247
- bucket: any;
248
- key: any;
249
- etag: any;
250
- checksumCRC32: any;
251
- checksumCRC32C: any;
252
- checksumCRC64NVME: any;
253
- checksumSHA1: any;
254
- checksumSHA256: any;
255
- checksumType: any;
256
- }>;
283
+ completeMultipartUpload(key: string, uploadId: string, parts: readonly MultipartUploadPart[], options?: CompleteMultipartUploadOptions): Promise<CompleteMultipartUploadResult>;
284
+ /**
285
+ * @remarks Uses [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html).
286
+ * @throws {RangeError} If `key` is not at least 1 character long.
287
+ * @throws {Error} If `uploadId` is not provided.
288
+ */
289
+ uploadPart(key: string, uploadId: string, data: UndiciBodyInit, partNumber: number, options?: UploadPartOptions): Promise<UploadPartResult>;
290
+ /**
291
+ * @remarks Uses [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html).
292
+ * @throws {RangeError} If `key` is not at least 1 character long.
293
+ * @throws {Error} If `uploadId` is not provided.
294
+ * @throws {TypeError} If `options.maxParts` is not a `number`.
295
+ * @throws {RangeError} If `options.maxParts` is <= 0.
296
+ * @throws {TypeError} If `options.partNumberMarker` is not a `string`.
297
+ */
298
+ listParts(key: string, uploadId: string, options?: ListPartsOptions): Promise<ListPartsResult>;
257
299
  /**
258
300
  * Creates a new bucket on the S3 server.
259
301
  *
@@ -293,7 +335,7 @@ declare class S3Client {
293
335
  *
294
336
  * @throws {RangeError} If `maxKeys` is not between `1` and `1000`.
295
337
  */
296
- list(options?: ListObjectsOptions): Promise<ListObjectsResponse>;
338
+ list(options?: ListObjectsOptions): Promise<ListObjectsResult>;
297
339
  /**
298
340
  * Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
299
341
  */
@@ -433,4 +475,4 @@ type BucketInfo = {
433
475
  type?: string;
434
476
  };
435
477
 
436
- export { type AbortMultipartUploadOptions, type Acl, type BucketCreationOptions, type BucketDeletionOptions, type BucketExistsOptions, type BucketInfo, type BucketLocationInfo, type ByteSource, type ChecksumAlgorithm, type ChecksumType, type CompleteMultipartUploadOptions, type CompleteMultipartUploadResult, type CreateFileInstanceOptions, type CreateMultipartUploadOptions, type CreateMultipartUploadResponse, type DeleteObjectsOptions, type HttpMethod, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResponse, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit };
478
+ export { type AbortMultipartUploadOptions, type Acl, type BucketCreationOptions, type BucketDeletionOptions, type BucketExistsOptions, type BucketInfo, type BucketLocationInfo, type ByteSource, type ChecksumAlgorithm, type ChecksumType, type CompleteMultipartUploadOptions, type CompleteMultipartUploadResult, type CreateFileInstanceOptions, type CreateMultipartUploadOptions, type CreateMultipartUploadResult, type DeleteObjectsOptions, type HttpMethod, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResult, type ListPartsOptions, type ListPartsResult, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit, type UploadPartOptions, type UploadPartResult };
package/dist/index.js CHANGED
@@ -324,7 +324,7 @@ var stream = Symbol("stream");
324
324
  var signedRequest = Symbol("signedRequest");
325
325
  var xmlParser2 = new XMLParser2({
326
326
  ignoreAttributes: true,
327
- isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
327
+ isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "ListPartsResult.Part" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
328
328
  });
329
329
  var xmlBuilder = new XMLBuilder({
330
330
  attributeNamePrefix: "$",
@@ -431,11 +431,17 @@ var S3Client = class {
431
431
  expiresIn = 3600,
432
432
  // TODO: Maybe rename this to expiresInSeconds
433
433
  storageClass,
434
+ contentLength,
434
435
  acl,
435
436
  region: regionOverride,
436
437
  bucket: bucketOverride,
437
438
  endpoint: endpointOverride
438
439
  } = {}) {
440
+ if (typeof contentLength === "number") {
441
+ if (contentLength < 0) {
442
+ throw new RangeError("`contentLength` must be >= 0.");
443
+ }
444
+ }
439
445
  const now2 = /* @__PURE__ */ new Date();
440
446
  const date = getAmzDate(now2);
441
447
  const options = this.#options;
@@ -447,13 +453,19 @@ var S3Client = class {
447
453
  `${options.accessKeyId}/${date.date}/${region}/s3/aws4_request`,
448
454
  date,
449
455
  expiresIn,
450
- "host",
451
- void 0,
456
+ typeof contentLength === "number" ? "content-length;host" : "host",
457
+ unsignedPayload,
452
458
  storageClass,
453
459
  options.sessionToken,
454
460
  acl
455
461
  );
456
- const dataDigest = createCanonicalDataDigestHostOnly(
462
+ const dataDigest = typeof contentLength === "number" ? createCanonicalDataDigest(
463
+ method,
464
+ res.pathname,
465
+ query,
466
+ { "content-length": String(contentLength), host: res.host },
467
+ unsignedPayload
468
+ ) : createCanonicalDataDigestHostOnly(
457
469
  method,
458
470
  res.pathname,
459
471
  query,
@@ -512,28 +524,28 @@ var S3Client = class {
512
524
  let query = "uploads=";
513
525
  if (options.delimiter) {
514
526
  if (typeof options.delimiter !== "string") {
515
- throw new TypeError("`delimiter` should be a `string`.");
527
+ throw new TypeError("`delimiter` must be a `string`.");
516
528
  }
517
529
  query += `&delimiter=${encodeURIComponent(options.delimiter)}`;
518
530
  }
519
531
  if (options.keyMarker) {
520
532
  if (typeof options.keyMarker !== "string") {
521
- throw new TypeError("`keyMarker` should be a `string`.");
533
+ throw new TypeError("`keyMarker` must be a `string`.");
522
534
  }
523
535
  query += `&key-marker=${encodeURIComponent(options.keyMarker)}`;
524
536
  }
525
537
  if (typeof options.maxUploads !== "undefined") {
526
538
  if (typeof options.maxUploads !== "number") {
527
- throw new TypeError("`maxUploads` should be a `number`.");
539
+ throw new TypeError("`maxUploads` must be a `number`.");
528
540
  }
529
541
  if (options.maxUploads < 1 || options.maxUploads > 1e3) {
530
- throw new RangeError("`maxUploads` should be between 1 and 1000.");
542
+ throw new RangeError("`maxUploads` has to be between 1 and 1000.");
531
543
  }
532
544
  query += `&max-uploads=${options.maxUploads}`;
533
545
  }
534
546
  if (options.prefix) {
535
547
  if (typeof options.prefix !== "string") {
536
- throw new TypeError("`prefix` should be a `string`.");
548
+ throw new TypeError("`prefix` must be a `string`.");
537
549
  }
538
550
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
539
551
  }
@@ -655,6 +667,112 @@ var S3Client = class {
655
667
  checksumType: res.ChecksumType || void 0
656
668
  };
657
669
  }
670
+ /**
671
+ * @remarks Uses [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html).
672
+ * @throws {RangeError} If `key` is not at least 1 character long.
673
+ * @throws {Error} If `uploadId` is not provided.
674
+ */
675
+ async uploadPart(key, uploadId, data, partNumber, options = {}) {
676
+ if (key.length < 1) {
677
+ throw new RangeError("`key` must be at least 1 character long.");
678
+ }
679
+ if (!uploadId) {
680
+ throw new Error("`uploadId` is required.");
681
+ }
682
+ if (!data) {
683
+ throw new Error("`data` is required.");
684
+ }
685
+ if (typeof partNumber !== "number" || partNumber <= 0) {
686
+ throw new Error("`partNumber` has to be a `number` which is >= 1.");
687
+ }
688
+ const response = await this[signedRequest](
689
+ "PUT",
690
+ key,
691
+ `partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
692
+ data,
693
+ void 0,
694
+ void 0,
695
+ void 0,
696
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
697
+ options.signal
698
+ );
699
+ if (response.statusCode === 200) {
700
+ await response.body.dump();
701
+ const etag = response.headers.etag;
702
+ if (typeof etag !== "string" || etag.length === 0) {
703
+ throw new S3Error("Unknown", "", {
704
+ message: "Response did not contain an etag."
705
+ });
706
+ }
707
+ return {
708
+ partNumber,
709
+ etag
710
+ };
711
+ }
712
+ throw await getResponseError(response, "");
713
+ }
714
+ /**
715
+ * @remarks Uses [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html).
716
+ * @throws {RangeError} If `key` is not at least 1 character long.
717
+ * @throws {Error} If `uploadId` is not provided.
718
+ * @throws {TypeError} If `options.maxParts` is not a `number`.
719
+ * @throws {RangeError} If `options.maxParts` is <= 0.
720
+ * @throws {TypeError} If `options.partNumberMarker` is not a `string`.
721
+ */
722
+ async listParts(key, uploadId, options = {}) {
723
+ let query = "";
724
+ if (options.maxParts) {
725
+ if (typeof options.maxParts !== "number") {
726
+ throw new TypeError("`maxParts` must be a `number`.");
727
+ }
728
+ if (options.maxParts <= 0) {
729
+ throw new RangeError("`maxParts` must be >= 1.");
730
+ }
731
+ query += `&max-parts=${options.maxParts}`;
732
+ }
733
+ if (options.partNumberMarker) {
734
+ if (typeof options.partNumberMarker !== "string") {
735
+ throw new TypeError("`partNumberMarker` must be a `string`.");
736
+ }
737
+ query += `&part-number-marker=${encodeURIComponent(options.partNumberMarker)}`;
738
+ }
739
+ query += `&uploadId=${encodeURIComponent(uploadId)}`;
740
+ const response = await this[signedRequest](
741
+ "GET",
742
+ key,
743
+ // We always have a leading &, so we can slice the leading & away (this way, we have less conditionals on the hot path); see benchmark-operations.js
744
+ query.substring(1),
745
+ void 0,
746
+ void 0,
747
+ void 0,
748
+ void 0,
749
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
750
+ options?.signal
751
+ );
752
+ if (response.statusCode === 200) {
753
+ const text = await response.body.text();
754
+ const root = ensureParsedXml(text).ListPartsResult ?? {};
755
+ return {
756
+ bucket: root.Bucket,
757
+ key: root.Key,
758
+ uploadId: root.UploadId,
759
+ partNumberMarker: root.PartNumberMarker ?? void 0,
760
+ nextPartNumberMarker: root.NextPartNumberMarker ?? void 0,
761
+ maxParts: root.MaxParts ?? 1e3,
762
+ isTruncated: root.IsTruncated ?? false,
763
+ parts: (
764
+ // biome-ignore lint/suspicious/noExplicitAny: parsing code
765
+ root.Part?.map((part) => ({
766
+ etag: part.ETag,
767
+ lastModified: part.LastModified ? new Date(part.LastModified) : void 0,
768
+ partNumber: part.PartNumber ?? void 0,
769
+ size: part.Size ?? void 0
770
+ })) ?? []
771
+ )
772
+ };
773
+ }
774
+ throw await getResponseError(response, key);
775
+ }
658
776
  //#endregion
659
777
  //#region bucket operations
660
778
  /**
@@ -800,29 +918,29 @@ var S3Client = class {
800
918
  let query = "";
801
919
  if (typeof options.continuationToken !== "undefined") {
802
920
  if (typeof options.continuationToken !== "string") {
803
- throw new TypeError("`continuationToken` should be a `string`.");
921
+ throw new TypeError("`continuationToken` must be a `string`.");
804
922
  }
805
923
  query += `continuation-token=${encodeURIComponent(options.continuationToken)}&`;
806
924
  }
807
925
  query += "list-type=2";
808
926
  if (typeof options.maxKeys !== "undefined") {
809
927
  if (typeof options.maxKeys !== "number") {
810
- throw new TypeError("`maxKeys` should be a `number`.");
928
+ throw new TypeError("`maxKeys` must be a `number`.");
811
929
  }
812
930
  if (options.maxKeys < 1 || options.maxKeys > 1e3) {
813
- throw new RangeError("`maxKeys` should be between 1 and 1000.");
931
+ throw new RangeError("`maxKeys` has to be between 1 and 1000.");
814
932
  }
815
933
  query += `&max-keys=${options.maxKeys}`;
816
934
  }
817
935
  if (options.prefix) {
818
936
  if (typeof options.prefix !== "string") {
819
- throw new TypeError("`prefix` should be a `string`.");
937
+ throw new TypeError("`prefix` must be a `string`.");
820
938
  }
821
939
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
822
940
  }
823
941
  if (typeof options.startAfter !== "undefined") {
824
942
  if (typeof options.startAfter !== "string") {
825
- throw new TypeError("`startAfter` should be a `string`.");
943
+ throw new TypeError("`startAfter` must be a `string`.");
826
944
  }
827
945
  query += `&start-after=${encodeURIComponent(options.startAfter)}`;
828
946
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "lean-s3",
3
3
  "author": "Niklas Mollenhauer",
4
4
  "license": "MIT",
5
- "version": "0.4.1",
5
+ "version": "0.6.0",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",
@@ -44,15 +44,16 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "fast-xml-parser": "^5.2.5",
47
+ "i": "^0.3.7",
47
48
  "undici": "^7.10.0"
48
49
  },
49
50
  "devDependencies": {
50
- "@biomejs/biome": "^2.0.0",
51
+ "@biomejs/biome": "2.0.0",
51
52
  "@testcontainers/localstack": "^11.0.3",
52
53
  "@testcontainers/minio": "^11.0.3",
53
54
  "@types/node": "^24.0.3",
54
- "@typescript/native-preview": "^7.0.0-dev.20250618.1",
55
- "expect": "^30.0.0",
55
+ "@typescript/native-preview": "^7.0.0-dev.20250620.1",
56
+ "expect": "^30.0.2",
56
57
  "lefthook": "^1.11.14",
57
58
  "tsup": "^8.5.0",
58
59
  "tsx": "^4.20.3",