lean-s3 0.5.0 → 0.6.1
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 +70 -20
- package/dist/index.js +158 -45
- 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
|
@@ -20,6 +20,14 @@ declare class S3BucketEntry {
|
|
|
20
20
|
static parse(source: any): S3BucketEntry;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
declare const __brand: unique symbol;
|
|
24
|
+
type Brand<B> = {
|
|
25
|
+
[__brand]: B;
|
|
26
|
+
};
|
|
27
|
+
type Branded<T, B> = T & Brand<B>;
|
|
28
|
+
type BucketName = Branded<string, "BucketName">;
|
|
29
|
+
type ObjectKey = Branded<string, "ObjectKey">;
|
|
30
|
+
|
|
23
31
|
declare const write: unique symbol;
|
|
24
32
|
declare const stream: unique symbol;
|
|
25
33
|
declare const signedRequest: unique symbol;
|
|
@@ -34,6 +42,7 @@ interface S3ClientOptions {
|
|
|
34
42
|
type OverridableS3ClientOptions = Pick<S3ClientOptions, "region" | "bucket" | "endpoint">;
|
|
35
43
|
type CreateFileInstanceOptions = {};
|
|
36
44
|
type DeleteObjectsOptions = {
|
|
45
|
+
bucket?: string;
|
|
37
46
|
signal?: AbortSignal;
|
|
38
47
|
};
|
|
39
48
|
interface S3FilePresignOptions {
|
|
@@ -100,7 +109,7 @@ type CreateMultipartUploadOptions = {
|
|
|
100
109
|
bucket?: string;
|
|
101
110
|
signal?: AbortSignal;
|
|
102
111
|
};
|
|
103
|
-
type
|
|
112
|
+
type CreateMultipartUploadResult = {
|
|
104
113
|
bucket: string;
|
|
105
114
|
key: string;
|
|
106
115
|
uploadId: string;
|
|
@@ -129,7 +138,44 @@ type MultipartUploadPart = {
|
|
|
129
138
|
partNumber: number;
|
|
130
139
|
etag: string;
|
|
131
140
|
};
|
|
132
|
-
type
|
|
141
|
+
type UploadPartOptions = {
|
|
142
|
+
bucket?: string;
|
|
143
|
+
signal?: AbortSignal;
|
|
144
|
+
};
|
|
145
|
+
type UploadPartResult = {
|
|
146
|
+
partNumber: number;
|
|
147
|
+
etag: string;
|
|
148
|
+
};
|
|
149
|
+
type ListPartsOptions = {
|
|
150
|
+
maxParts?: number;
|
|
151
|
+
partNumberMarker?: string;
|
|
152
|
+
bucket?: string;
|
|
153
|
+
signal?: AbortSignal;
|
|
154
|
+
};
|
|
155
|
+
type ListPartsResult = {
|
|
156
|
+
bucket: string;
|
|
157
|
+
key: string;
|
|
158
|
+
uploadId: string;
|
|
159
|
+
partNumberMarker?: string;
|
|
160
|
+
nextPartNumberMarker?: string;
|
|
161
|
+
maxParts?: number;
|
|
162
|
+
isTruncated: boolean;
|
|
163
|
+
parts: Array<{
|
|
164
|
+
checksumCRC32?: string;
|
|
165
|
+
checksumCRC32C?: string;
|
|
166
|
+
checksumCRC64NVME?: string;
|
|
167
|
+
checksumSHA1?: string;
|
|
168
|
+
checksumSHA256?: string;
|
|
169
|
+
etag: string;
|
|
170
|
+
lastModified: Date;
|
|
171
|
+
partNumber: number;
|
|
172
|
+
size: number;
|
|
173
|
+
}>;
|
|
174
|
+
storageClass?: StorageClass;
|
|
175
|
+
checksumAlgorithm?: ChecksumAlgorithm;
|
|
176
|
+
checksumType?: ChecksumType;
|
|
177
|
+
};
|
|
178
|
+
type ListObjectsResult = {
|
|
133
179
|
name: string;
|
|
134
180
|
prefix: string | undefined;
|
|
135
181
|
startAfter: string | undefined;
|
|
@@ -226,7 +272,7 @@ declare class S3Client {
|
|
|
226
272
|
*/
|
|
227
273
|
presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
|
|
228
274
|
storageClass, contentLength, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
|
|
229
|
-
createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<
|
|
275
|
+
createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
|
|
230
276
|
/**
|
|
231
277
|
* @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
|
|
232
278
|
* @throws {RangeError} If `options.maxKeys` is not between `1` and `1000`.
|
|
@@ -237,24 +283,28 @@ declare class S3Client {
|
|
|
237
283
|
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
238
284
|
* @throws {Error} If `uploadId` is not provided.
|
|
239
285
|
*/
|
|
240
|
-
abortMultipartUpload(
|
|
286
|
+
abortMultipartUpload(path: string, uploadId: string, options?: AbortMultipartUploadOptions): Promise<void>;
|
|
241
287
|
/**
|
|
242
288
|
* @remarks Uses [`CompleteMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html).
|
|
243
289
|
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
244
290
|
* @throws {Error} If `uploadId` is not provided.
|
|
245
291
|
*/
|
|
246
|
-
completeMultipartUpload(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
completeMultipartUpload(path: string, uploadId: string, parts: readonly MultipartUploadPart[], options?: CompleteMultipartUploadOptions): Promise<CompleteMultipartUploadResult>;
|
|
293
|
+
/**
|
|
294
|
+
* @remarks Uses [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html).
|
|
295
|
+
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
296
|
+
* @throws {Error} If `uploadId` is not provided.
|
|
297
|
+
*/
|
|
298
|
+
uploadPart(path: string, uploadId: string, data: UndiciBodyInit, partNumber: number, options?: UploadPartOptions): Promise<UploadPartResult>;
|
|
299
|
+
/**
|
|
300
|
+
* @remarks Uses [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html).
|
|
301
|
+
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
302
|
+
* @throws {Error} If `uploadId` is not provided.
|
|
303
|
+
* @throws {TypeError} If `options.maxParts` is not a `number`.
|
|
304
|
+
* @throws {RangeError} If `options.maxParts` is <= 0.
|
|
305
|
+
* @throws {TypeError} If `options.partNumberMarker` is not a `string`.
|
|
306
|
+
*/
|
|
307
|
+
listParts(path: string, uploadId: string, options?: ListPartsOptions): Promise<ListPartsResult>;
|
|
258
308
|
/**
|
|
259
309
|
* Creates a new bucket on the S3 server.
|
|
260
310
|
*
|
|
@@ -294,7 +344,7 @@ declare class S3Client {
|
|
|
294
344
|
*
|
|
295
345
|
* @throws {RangeError} If `maxKeys` is not between `1` and `1000`.
|
|
296
346
|
*/
|
|
297
|
-
list(options?: ListObjectsOptions): Promise<
|
|
347
|
+
list(options?: ListObjectsOptions): Promise<ListObjectsResult>;
|
|
298
348
|
/**
|
|
299
349
|
* Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
|
|
300
350
|
*/
|
|
@@ -306,7 +356,7 @@ declare class S3Client {
|
|
|
306
356
|
* TODO: Maybe move this into a separate free function?
|
|
307
357
|
* @internal
|
|
308
358
|
*/
|
|
309
|
-
[signedRequest](method: HttpMethod, pathWithoutBucket:
|
|
359
|
+
[signedRequest](method: HttpMethod, pathWithoutBucket: ObjectKey, query: string | undefined, body: UndiciBodyInit | undefined, additionalSignedHeaders: Record<string, string> | undefined, additionalUnsignedHeaders: Record<string, string> | undefined, contentHash: Buffer | undefined, bucket: BucketName | undefined, signal?: AbortSignal | undefined): Promise<Dispatcher.ResponseData<null>>;
|
|
310
360
|
/**
|
|
311
361
|
* @internal
|
|
312
362
|
* @param {import("./index.d.ts").UndiciBodyInit} data TODO
|
|
@@ -332,7 +382,7 @@ declare class S3File {
|
|
|
332
382
|
/**
|
|
333
383
|
* @internal
|
|
334
384
|
*/
|
|
335
|
-
constructor(client: S3Client, path:
|
|
385
|
+
constructor(client: S3Client, path: ObjectKey, start: number | undefined, end: number | undefined, contentType: string | undefined);
|
|
336
386
|
slice(start?: number | undefined, end?: number | undefined, contentType?: string | undefined): S3File;
|
|
337
387
|
/**
|
|
338
388
|
* Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
|
|
@@ -434,4 +484,4 @@ type BucketInfo = {
|
|
|
434
484
|
type?: string;
|
|
435
485
|
};
|
|
436
486
|
|
|
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
|
|
487
|
+
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
|
@@ -318,13 +318,41 @@ function getAuthorizationHeader(keyCache, method, path, query, date, sortedSigne
|
|
|
318
318
|
return `AWS4-HMAC-SHA256 Credential=${credentialSpec}, SignedHeaders=${signedHeadersSpec}, Signature=${signature}`;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// src/branded.ts
|
|
322
|
+
function ensureValidBucketName(name) {
|
|
323
|
+
if (name.length < 3 || name.length > 63) {
|
|
324
|
+
throw new Error("`name` must be between 3 and 63 characters long.");
|
|
325
|
+
}
|
|
326
|
+
if (name.startsWith(".") || name.endsWith(".")) {
|
|
327
|
+
throw new Error("`name` must not start or end with a period (.)");
|
|
328
|
+
}
|
|
329
|
+
if (!/^[a-z0-9.-]+$/.test(name)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
"`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (name.includes("..")) {
|
|
335
|
+
throw new Error("`name` must not contain two adjacent periods (..)");
|
|
336
|
+
}
|
|
337
|
+
return name;
|
|
338
|
+
}
|
|
339
|
+
function ensureValidPath(path) {
|
|
340
|
+
if (typeof path !== "string") {
|
|
341
|
+
throw new TypeError("`path` must be a `string`.");
|
|
342
|
+
}
|
|
343
|
+
if (path.length < 1) {
|
|
344
|
+
throw new RangeError("`path` must be at least 1 character long.");
|
|
345
|
+
}
|
|
346
|
+
return path;
|
|
347
|
+
}
|
|
348
|
+
|
|
321
349
|
// src/S3Client.ts
|
|
322
350
|
var write = Symbol("write");
|
|
323
351
|
var stream = Symbol("stream");
|
|
324
352
|
var signedRequest = Symbol("signedRequest");
|
|
325
353
|
var xmlParser2 = new XMLParser2({
|
|
326
354
|
ignoreAttributes: true,
|
|
327
|
-
isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
|
|
355
|
+
isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "ListPartsResult.Part" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
|
|
328
356
|
});
|
|
329
357
|
var xmlBuilder = new XMLBuilder({
|
|
330
358
|
attributeNamePrefix: "$",
|
|
@@ -412,7 +440,13 @@ var S3Client = class {
|
|
|
412
440
|
* ```
|
|
413
441
|
*/
|
|
414
442
|
file(path, _options) {
|
|
415
|
-
return new S3File(
|
|
443
|
+
return new S3File(
|
|
444
|
+
this,
|
|
445
|
+
ensureValidPath(path),
|
|
446
|
+
void 0,
|
|
447
|
+
void 0,
|
|
448
|
+
void 0
|
|
449
|
+
);
|
|
416
450
|
}
|
|
417
451
|
/**
|
|
418
452
|
* Generate a presigned URL for temporary access to a file.
|
|
@@ -489,11 +523,10 @@ var S3Client = class {
|
|
|
489
523
|
//#region multipart uploads
|
|
490
524
|
async createMultipartUpload(key, options = {}) {
|
|
491
525
|
if (key.length < 1) {
|
|
492
|
-
throw new RangeError("`key` must be at least 1 character long.");
|
|
493
526
|
}
|
|
494
527
|
const response = await this[signedRequest](
|
|
495
528
|
"POST",
|
|
496
|
-
key,
|
|
529
|
+
ensureValidPath(key),
|
|
497
530
|
"uploads=",
|
|
498
531
|
void 0,
|
|
499
532
|
void 0,
|
|
@@ -524,28 +557,28 @@ var S3Client = class {
|
|
|
524
557
|
let query = "uploads=";
|
|
525
558
|
if (options.delimiter) {
|
|
526
559
|
if (typeof options.delimiter !== "string") {
|
|
527
|
-
throw new TypeError("`delimiter`
|
|
560
|
+
throw new TypeError("`delimiter` must be a `string`.");
|
|
528
561
|
}
|
|
529
562
|
query += `&delimiter=${encodeURIComponent(options.delimiter)}`;
|
|
530
563
|
}
|
|
531
564
|
if (options.keyMarker) {
|
|
532
565
|
if (typeof options.keyMarker !== "string") {
|
|
533
|
-
throw new TypeError("`keyMarker`
|
|
566
|
+
throw new TypeError("`keyMarker` must be a `string`.");
|
|
534
567
|
}
|
|
535
568
|
query += `&key-marker=${encodeURIComponent(options.keyMarker)}`;
|
|
536
569
|
}
|
|
537
570
|
if (typeof options.maxUploads !== "undefined") {
|
|
538
571
|
if (typeof options.maxUploads !== "number") {
|
|
539
|
-
throw new TypeError("`maxUploads`
|
|
572
|
+
throw new TypeError("`maxUploads` must be a `number`.");
|
|
540
573
|
}
|
|
541
574
|
if (options.maxUploads < 1 || options.maxUploads > 1e3) {
|
|
542
|
-
throw new RangeError("`maxUploads`
|
|
575
|
+
throw new RangeError("`maxUploads` has to be between 1 and 1000.");
|
|
543
576
|
}
|
|
544
577
|
query += `&max-uploads=${options.maxUploads}`;
|
|
545
578
|
}
|
|
546
579
|
if (options.prefix) {
|
|
547
580
|
if (typeof options.prefix !== "string") {
|
|
548
|
-
throw new TypeError("`prefix`
|
|
581
|
+
throw new TypeError("`prefix` must be a `string`.");
|
|
549
582
|
}
|
|
550
583
|
query += `&prefix=${encodeURIComponent(options.prefix)}`;
|
|
551
584
|
}
|
|
@@ -596,16 +629,13 @@ var S3Client = class {
|
|
|
596
629
|
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
597
630
|
* @throws {Error} If `uploadId` is not provided.
|
|
598
631
|
*/
|
|
599
|
-
async abortMultipartUpload(
|
|
600
|
-
if (key.length < 1) {
|
|
601
|
-
throw new RangeError("`key` must be at least 1 character long.");
|
|
602
|
-
}
|
|
632
|
+
async abortMultipartUpload(path, uploadId, options = {}) {
|
|
603
633
|
if (!uploadId) {
|
|
604
634
|
throw new Error("`uploadId` is required.");
|
|
605
635
|
}
|
|
606
636
|
const response = await this[signedRequest](
|
|
607
637
|
"DELETE",
|
|
608
|
-
|
|
638
|
+
ensureValidPath(path),
|
|
609
639
|
`uploadId=${encodeURIComponent(uploadId)}`,
|
|
610
640
|
void 0,
|
|
611
641
|
void 0,
|
|
@@ -615,7 +645,7 @@ var S3Client = class {
|
|
|
615
645
|
options.signal
|
|
616
646
|
);
|
|
617
647
|
if (response.statusCode !== 204) {
|
|
618
|
-
throw await getResponseError(response,
|
|
648
|
+
throw await getResponseError(response, path);
|
|
619
649
|
}
|
|
620
650
|
}
|
|
621
651
|
/**
|
|
@@ -623,10 +653,7 @@ var S3Client = class {
|
|
|
623
653
|
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
624
654
|
* @throws {Error} If `uploadId` is not provided.
|
|
625
655
|
*/
|
|
626
|
-
async completeMultipartUpload(
|
|
627
|
-
if (key.length < 1) {
|
|
628
|
-
throw new RangeError("`key` must be at least 1 character long.");
|
|
629
|
-
}
|
|
656
|
+
async completeMultipartUpload(path, uploadId, parts, options = {}) {
|
|
630
657
|
if (!uploadId) {
|
|
631
658
|
throw new Error("`uploadId` is required.");
|
|
632
659
|
}
|
|
@@ -640,7 +667,7 @@ var S3Client = class {
|
|
|
640
667
|
});
|
|
641
668
|
const response = await this[signedRequest](
|
|
642
669
|
"POST",
|
|
643
|
-
|
|
670
|
+
ensureValidPath(path),
|
|
644
671
|
`uploadId=${encodeURIComponent(uploadId)}`,
|
|
645
672
|
body,
|
|
646
673
|
void 0,
|
|
@@ -650,7 +677,7 @@ var S3Client = class {
|
|
|
650
677
|
options.signal
|
|
651
678
|
);
|
|
652
679
|
if (response.statusCode !== 200) {
|
|
653
|
-
throw await getResponseError(response,
|
|
680
|
+
throw await getResponseError(response, path);
|
|
654
681
|
}
|
|
655
682
|
const text = await response.body.text();
|
|
656
683
|
const res = ensureParsedXml(text).CompleteMultipartUploadResult ?? {};
|
|
@@ -667,6 +694,109 @@ var S3Client = class {
|
|
|
667
694
|
checksumType: res.ChecksumType || void 0
|
|
668
695
|
};
|
|
669
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* @remarks Uses [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html).
|
|
699
|
+
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
700
|
+
* @throws {Error} If `uploadId` is not provided.
|
|
701
|
+
*/
|
|
702
|
+
async uploadPart(path, uploadId, data, partNumber, options = {}) {
|
|
703
|
+
if (!uploadId) {
|
|
704
|
+
throw new Error("`uploadId` is required.");
|
|
705
|
+
}
|
|
706
|
+
if (!data) {
|
|
707
|
+
throw new Error("`data` is required.");
|
|
708
|
+
}
|
|
709
|
+
if (typeof partNumber !== "number" || partNumber <= 0) {
|
|
710
|
+
throw new Error("`partNumber` has to be a `number` which is >= 1.");
|
|
711
|
+
}
|
|
712
|
+
const response = await this[signedRequest](
|
|
713
|
+
"PUT",
|
|
714
|
+
ensureValidPath(path),
|
|
715
|
+
`partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
|
|
716
|
+
data,
|
|
717
|
+
void 0,
|
|
718
|
+
void 0,
|
|
719
|
+
void 0,
|
|
720
|
+
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
721
|
+
options.signal
|
|
722
|
+
);
|
|
723
|
+
if (response.statusCode === 200) {
|
|
724
|
+
await response.body.dump();
|
|
725
|
+
const etag = response.headers.etag;
|
|
726
|
+
if (typeof etag !== "string" || etag.length === 0) {
|
|
727
|
+
throw new S3Error("Unknown", "", {
|
|
728
|
+
message: "Response did not contain an etag."
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
partNumber,
|
|
733
|
+
etag
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
throw await getResponseError(response, "");
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* @remarks Uses [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html).
|
|
740
|
+
* @throws {RangeError} If `key` is not at least 1 character long.
|
|
741
|
+
* @throws {Error} If `uploadId` is not provided.
|
|
742
|
+
* @throws {TypeError} If `options.maxParts` is not a `number`.
|
|
743
|
+
* @throws {RangeError} If `options.maxParts` is <= 0.
|
|
744
|
+
* @throws {TypeError} If `options.partNumberMarker` is not a `string`.
|
|
745
|
+
*/
|
|
746
|
+
async listParts(path, uploadId, options = {}) {
|
|
747
|
+
let query = "";
|
|
748
|
+
if (options.maxParts) {
|
|
749
|
+
if (typeof options.maxParts !== "number") {
|
|
750
|
+
throw new TypeError("`maxParts` must be a `number`.");
|
|
751
|
+
}
|
|
752
|
+
if (options.maxParts <= 0) {
|
|
753
|
+
throw new RangeError("`maxParts` must be >= 1.");
|
|
754
|
+
}
|
|
755
|
+
query += `&max-parts=${options.maxParts}`;
|
|
756
|
+
}
|
|
757
|
+
if (options.partNumberMarker) {
|
|
758
|
+
if (typeof options.partNumberMarker !== "string") {
|
|
759
|
+
throw new TypeError("`partNumberMarker` must be a `string`.");
|
|
760
|
+
}
|
|
761
|
+
query += `&part-number-marker=${encodeURIComponent(options.partNumberMarker)}`;
|
|
762
|
+
}
|
|
763
|
+
query += `&uploadId=${encodeURIComponent(uploadId)}`;
|
|
764
|
+
const response = await this[signedRequest](
|
|
765
|
+
"GET",
|
|
766
|
+
ensureValidPath(path),
|
|
767
|
+
// 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
|
|
768
|
+
query.substring(1),
|
|
769
|
+
void 0,
|
|
770
|
+
void 0,
|
|
771
|
+
void 0,
|
|
772
|
+
void 0,
|
|
773
|
+
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
774
|
+
options?.signal
|
|
775
|
+
);
|
|
776
|
+
if (response.statusCode === 200) {
|
|
777
|
+
const text = await response.body.text();
|
|
778
|
+
const root = ensureParsedXml(text).ListPartsResult ?? {};
|
|
779
|
+
return {
|
|
780
|
+
bucket: root.Bucket,
|
|
781
|
+
key: root.Key,
|
|
782
|
+
uploadId: root.UploadId,
|
|
783
|
+
partNumberMarker: root.PartNumberMarker ?? void 0,
|
|
784
|
+
nextPartNumberMarker: root.NextPartNumberMarker ?? void 0,
|
|
785
|
+
maxParts: root.MaxParts ?? 1e3,
|
|
786
|
+
isTruncated: root.IsTruncated ?? false,
|
|
787
|
+
parts: (
|
|
788
|
+
// biome-ignore lint/suspicious/noExplicitAny: parsing code
|
|
789
|
+
root.Part?.map((part) => ({
|
|
790
|
+
etag: part.ETag,
|
|
791
|
+
lastModified: part.LastModified ? new Date(part.LastModified) : void 0,
|
|
792
|
+
partNumber: part.PartNumber ?? void 0,
|
|
793
|
+
size: part.Size ?? void 0
|
|
794
|
+
})) ?? []
|
|
795
|
+
)
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
throw await getResponseError(response, path);
|
|
799
|
+
}
|
|
670
800
|
//#endregion
|
|
671
801
|
//#region bucket operations
|
|
672
802
|
/**
|
|
@@ -812,29 +942,29 @@ var S3Client = class {
|
|
|
812
942
|
let query = "";
|
|
813
943
|
if (typeof options.continuationToken !== "undefined") {
|
|
814
944
|
if (typeof options.continuationToken !== "string") {
|
|
815
|
-
throw new TypeError("`continuationToken`
|
|
945
|
+
throw new TypeError("`continuationToken` must be a `string`.");
|
|
816
946
|
}
|
|
817
947
|
query += `continuation-token=${encodeURIComponent(options.continuationToken)}&`;
|
|
818
948
|
}
|
|
819
949
|
query += "list-type=2";
|
|
820
950
|
if (typeof options.maxKeys !== "undefined") {
|
|
821
951
|
if (typeof options.maxKeys !== "number") {
|
|
822
|
-
throw new TypeError("`maxKeys`
|
|
952
|
+
throw new TypeError("`maxKeys` must be a `number`.");
|
|
823
953
|
}
|
|
824
954
|
if (options.maxKeys < 1 || options.maxKeys > 1e3) {
|
|
825
|
-
throw new RangeError("`maxKeys`
|
|
955
|
+
throw new RangeError("`maxKeys` has to be between 1 and 1000.");
|
|
826
956
|
}
|
|
827
957
|
query += `&max-keys=${options.maxKeys}`;
|
|
828
958
|
}
|
|
829
959
|
if (options.prefix) {
|
|
830
960
|
if (typeof options.prefix !== "string") {
|
|
831
|
-
throw new TypeError("`prefix`
|
|
961
|
+
throw new TypeError("`prefix` must be a `string`.");
|
|
832
962
|
}
|
|
833
963
|
query += `&prefix=${encodeURIComponent(options.prefix)}`;
|
|
834
964
|
}
|
|
835
965
|
if (typeof options.startAfter !== "undefined") {
|
|
836
966
|
if (typeof options.startAfter !== "string") {
|
|
837
|
-
throw new TypeError("`startAfter`
|
|
967
|
+
throw new TypeError("`startAfter` must be a `string`.");
|
|
838
968
|
}
|
|
839
969
|
query += `&start-after=${encodeURIComponent(options.startAfter)}`;
|
|
840
970
|
}
|
|
@@ -846,7 +976,7 @@ var S3Client = class {
|
|
|
846
976
|
void 0,
|
|
847
977
|
void 0,
|
|
848
978
|
void 0,
|
|
849
|
-
options.bucket ?? this.#options.bucket,
|
|
979
|
+
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
850
980
|
options.signal
|
|
851
981
|
);
|
|
852
982
|
if (response.statusCode !== 200) {
|
|
@@ -898,7 +1028,7 @@ var S3Client = class {
|
|
|
898
1028
|
},
|
|
899
1029
|
void 0,
|
|
900
1030
|
void 0,
|
|
901
|
-
this.#options.bucket,
|
|
1031
|
+
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
902
1032
|
options.signal
|
|
903
1033
|
);
|
|
904
1034
|
if (response.statusCode === 200) {
|
|
@@ -1193,23 +1323,6 @@ function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHa
|
|
|
1193
1323
|
}
|
|
1194
1324
|
return res;
|
|
1195
1325
|
}
|
|
1196
|
-
function ensureValidBucketName(name) {
|
|
1197
|
-
if (name.length < 3 || name.length > 63) {
|
|
1198
|
-
throw new Error("`name` must be between 3 and 63 characters long.");
|
|
1199
|
-
}
|
|
1200
|
-
if (name.startsWith(".") || name.endsWith(".")) {
|
|
1201
|
-
throw new Error("`name` must not start or end with a period (.)");
|
|
1202
|
-
}
|
|
1203
|
-
if (!/^[a-z0-9.-]+$/.test(name)) {
|
|
1204
|
-
throw new Error(
|
|
1205
|
-
"`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
if (name.includes("..")) {
|
|
1209
|
-
throw new Error("`name` must not contain two adjacent periods (..)");
|
|
1210
|
-
}
|
|
1211
|
-
return name;
|
|
1212
|
-
}
|
|
1213
1326
|
function ensureParsedXml(text) {
|
|
1214
1327
|
try {
|
|
1215
1328
|
const r = xmlParser2.parse(text);
|
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.1",
|
|
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",
|