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 +2 -0
- package/dist/index.d.ts +58 -17
- package/dist/index.js +117 -11
- package/package.json +4 -3
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
|
|
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
|
|
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<
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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<
|
|
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
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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
|
+
"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": "
|
|
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.
|
|
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",
|