lean-s3 0.5.0 → 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
@@ -100,7 +100,7 @@ type CreateMultipartUploadOptions = {
100
100
  bucket?: string;
101
101
  signal?: AbortSignal;
102
102
  };
103
- type CreateMultipartUploadResponse = {
103
+ type CreateMultipartUploadResult = {
104
104
  bucket: string;
105
105
  key: string;
106
106
  uploadId: string;
@@ -129,7 +129,44 @@ type MultipartUploadPart = {
129
129
  partNumber: number;
130
130
  etag: string;
131
131
  };
132
- 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 = {
133
170
  name: string;
134
171
  prefix: string | undefined;
135
172
  startAfter: string | undefined;
@@ -226,7 +263,7 @@ declare class S3Client {
226
263
  */
227
264
  presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
228
265
  storageClass, contentLength, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
229
- createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResponse>;
266
+ createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
230
267
  /**
231
268
  * @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
232
269
  * @throws {RangeError} If `options.maxKeys` is not between `1` and `1000`.
@@ -243,18 +280,22 @@ declare class S3Client {
243
280
  * @throws {RangeError} If `key` is not at least 1 character long.
244
281
  * @throws {Error} If `uploadId` is not provided.
245
282
  */
246
- completeMultipartUpload(key: string, uploadId: string, parts: readonly MultipartUploadPart[], options?: CompleteMultipartUploadOptions): Promise<{
247
- location: any;
248
- bucket: any;
249
- key: any;
250
- etag: any;
251
- checksumCRC32: any;
252
- checksumCRC32C: any;
253
- checksumCRC64NVME: any;
254
- checksumSHA1: any;
255
- checksumSHA256: any;
256
- checksumType: any;
257
- }>;
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>;
258
299
  /**
259
300
  * Creates a new bucket on the S3 server.
260
301
  *
@@ -294,7 +335,7 @@ declare class S3Client {
294
335
  *
295
336
  * @throws {RangeError} If `maxKeys` is not between `1` and `1000`.
296
337
  */
297
- list(options?: ListObjectsOptions): Promise<ListObjectsResponse>;
338
+ list(options?: ListObjectsOptions): Promise<ListObjectsResult>;
298
339
  /**
299
340
  * Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
300
341
  */
@@ -434,4 +475,4 @@ type BucketInfo = {
434
475
  type?: string;
435
476
  };
436
477
 
437
- 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: "$",
@@ -524,28 +524,28 @@ var S3Client = class {
524
524
  let query = "uploads=";
525
525
  if (options.delimiter) {
526
526
  if (typeof options.delimiter !== "string") {
527
- throw new TypeError("`delimiter` should be a `string`.");
527
+ throw new TypeError("`delimiter` must be a `string`.");
528
528
  }
529
529
  query += `&delimiter=${encodeURIComponent(options.delimiter)}`;
530
530
  }
531
531
  if (options.keyMarker) {
532
532
  if (typeof options.keyMarker !== "string") {
533
- throw new TypeError("`keyMarker` should be a `string`.");
533
+ throw new TypeError("`keyMarker` must be a `string`.");
534
534
  }
535
535
  query += `&key-marker=${encodeURIComponent(options.keyMarker)}`;
536
536
  }
537
537
  if (typeof options.maxUploads !== "undefined") {
538
538
  if (typeof options.maxUploads !== "number") {
539
- throw new TypeError("`maxUploads` should be a `number`.");
539
+ throw new TypeError("`maxUploads` must be a `number`.");
540
540
  }
541
541
  if (options.maxUploads < 1 || options.maxUploads > 1e3) {
542
- throw new RangeError("`maxUploads` should be between 1 and 1000.");
542
+ throw new RangeError("`maxUploads` has to be between 1 and 1000.");
543
543
  }
544
544
  query += `&max-uploads=${options.maxUploads}`;
545
545
  }
546
546
  if (options.prefix) {
547
547
  if (typeof options.prefix !== "string") {
548
- throw new TypeError("`prefix` should be a `string`.");
548
+ throw new TypeError("`prefix` must be a `string`.");
549
549
  }
550
550
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
551
551
  }
@@ -667,6 +667,112 @@ var S3Client = class {
667
667
  checksumType: res.ChecksumType || void 0
668
668
  };
669
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
+ }
670
776
  //#endregion
671
777
  //#region bucket operations
672
778
  /**
@@ -812,29 +918,29 @@ var S3Client = class {
812
918
  let query = "";
813
919
  if (typeof options.continuationToken !== "undefined") {
814
920
  if (typeof options.continuationToken !== "string") {
815
- throw new TypeError("`continuationToken` should be a `string`.");
921
+ throw new TypeError("`continuationToken` must be a `string`.");
816
922
  }
817
923
  query += `continuation-token=${encodeURIComponent(options.continuationToken)}&`;
818
924
  }
819
925
  query += "list-type=2";
820
926
  if (typeof options.maxKeys !== "undefined") {
821
927
  if (typeof options.maxKeys !== "number") {
822
- throw new TypeError("`maxKeys` should be a `number`.");
928
+ throw new TypeError("`maxKeys` must be a `number`.");
823
929
  }
824
930
  if (options.maxKeys < 1 || options.maxKeys > 1e3) {
825
- throw new RangeError("`maxKeys` should be between 1 and 1000.");
931
+ throw new RangeError("`maxKeys` has to be between 1 and 1000.");
826
932
  }
827
933
  query += `&max-keys=${options.maxKeys}`;
828
934
  }
829
935
  if (options.prefix) {
830
936
  if (typeof options.prefix !== "string") {
831
- throw new TypeError("`prefix` should be a `string`.");
937
+ throw new TypeError("`prefix` must be a `string`.");
832
938
  }
833
939
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
834
940
  }
835
941
  if (typeof options.startAfter !== "undefined") {
836
942
  if (typeof options.startAfter !== "string") {
837
- throw new TypeError("`startAfter` should be a `string`.");
943
+ throw new TypeError("`startAfter` must be a `string`.");
838
944
  }
839
945
  query += `&start-after=${encodeURIComponent(options.startAfter)}`;
840
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.5.0",
5
+ "version": "0.6.0",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",
@@ -44,14 +44,15 @@
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.20250619.1",
55
+ "@typescript/native-preview": "^7.0.0-dev.20250620.1",
55
56
  "expect": "^30.0.2",
56
57
  "lefthook": "^1.11.14",
57
58
  "tsup": "^8.5.0",