lean-s3 0.7.3 → 0.7.5
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 +10 -0
- package/dist/index.d.ts +27 -8
- package/dist/index.js +174 -136
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -181,3 +181,13 @@ const client = new S3Client({
|
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
Popular S3 provider missing? Open an issue or file a PR!
|
|
184
|
+
|
|
185
|
+
## Tested On
|
|
186
|
+
To ensure compability across various providers and self-hosted services, all tests are run on:
|
|
187
|
+
- Amazon AWS S3
|
|
188
|
+
- Hetzner Object Storage
|
|
189
|
+
- Cloudflare R2
|
|
190
|
+
- Garage
|
|
191
|
+
- Minio
|
|
192
|
+
- LocalStack
|
|
193
|
+
- Ceph
|
package/dist/index.d.ts
CHANGED
|
@@ -27,10 +27,13 @@ type Brand<B> = {
|
|
|
27
27
|
type Branded<T, B> = T & Brand<B>;
|
|
28
28
|
type BucketName = Branded<string, "BucketName">;
|
|
29
29
|
type ObjectKey = Branded<string, "ObjectKey">;
|
|
30
|
+
type Endpoint = Branded<string, "Endpoint">;
|
|
31
|
+
type Region = Branded<string, "Region">;
|
|
30
32
|
|
|
31
|
-
declare const
|
|
32
|
-
declare const
|
|
33
|
-
declare const
|
|
33
|
+
declare const kWrite: unique symbol;
|
|
34
|
+
declare const kStream: unique symbol;
|
|
35
|
+
declare const kSignedRequest: unique symbol;
|
|
36
|
+
declare const kGetEffectiveParams: unique symbol;
|
|
34
37
|
interface S3ClientOptions {
|
|
35
38
|
bucket: string;
|
|
36
39
|
region: string;
|
|
@@ -223,6 +226,8 @@ type ListObjectsResult = {
|
|
|
223
226
|
contents: readonly S3BucketEntry[];
|
|
224
227
|
};
|
|
225
228
|
type BucketCreationOptions = {
|
|
229
|
+
endpoint?: string;
|
|
230
|
+
region?: string;
|
|
226
231
|
locationConstraint?: string;
|
|
227
232
|
location?: BucketLocationInfo;
|
|
228
233
|
info?: BucketInfo;
|
|
@@ -287,6 +292,8 @@ declare class S3Client {
|
|
|
287
292
|
* @param options The default options to use for the S3 client.
|
|
288
293
|
*/
|
|
289
294
|
constructor(options: S3ClientOptions);
|
|
295
|
+
/** @internal */
|
|
296
|
+
[kGetEffectiveParams](options: OverridableS3ClientOptions): [region: Region, endpoint: Endpoint, bucket: BucketName];
|
|
290
297
|
/**
|
|
291
298
|
* Creates an S3File instance for the given path.
|
|
292
299
|
*
|
|
@@ -346,7 +353,7 @@ declare class S3Client {
|
|
|
346
353
|
* });
|
|
347
354
|
* ```
|
|
348
355
|
*/
|
|
349
|
-
presign(path: string,
|
|
356
|
+
presign(path: string, options?: S3FilePresignOptions): string;
|
|
350
357
|
createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
|
|
351
358
|
/**
|
|
352
359
|
* @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
|
|
@@ -441,16 +448,16 @@ declare class S3Client {
|
|
|
441
448
|
* TODO: Maybe move this into a separate free function?
|
|
442
449
|
* @internal
|
|
443
450
|
*/
|
|
444
|
-
[
|
|
451
|
+
[kSignedRequest](region: Region, endpoint: Endpoint, bucket: BucketName, method: HttpMethod, pathWithoutBucket: ObjectKey, query: string | undefined, body: UndiciBodyInit | undefined, additionalSignedHeaders: Record<string, string> | undefined, additionalUnsignedHeaders: Record<string, string> | undefined, contentHash: Buffer | undefined, signal: AbortSignal | undefined): Promise<Dispatcher.ResponseData<null>>;
|
|
445
452
|
/**
|
|
446
453
|
* @internal
|
|
447
454
|
* @param {import("./index.d.ts").UndiciBodyInit} data TODO
|
|
448
455
|
*/
|
|
449
|
-
[
|
|
456
|
+
[kWrite](path: ObjectKey, data: UndiciBodyInit, contentType: string, contentLength: number | undefined, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined, signal?: AbortSignal | undefined): Promise<void>;
|
|
450
457
|
/**
|
|
451
458
|
* @internal
|
|
452
459
|
*/
|
|
453
|
-
[
|
|
460
|
+
[kStream](path: ObjectKey, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined): stream_web.ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
declare class S3Stat {
|
|
@@ -470,7 +477,19 @@ declare class S3File {
|
|
|
470
477
|
#private;
|
|
471
478
|
/** @internal */
|
|
472
479
|
constructor(client: S3Client, path: ObjectKey, start: number | undefined, end: number | undefined, contentType: string | undefined);
|
|
473
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Creates and returns a new {@link S3File} containing a subset of this {@link S3File} data.
|
|
482
|
+
* @param start The starting index.
|
|
483
|
+
* @param end The ending index, exclusive.
|
|
484
|
+
*/
|
|
485
|
+
slice(start?: number, end?: number): S3File;
|
|
486
|
+
/**
|
|
487
|
+
* Creates and returns a new {@link S3File} containing a subset of this {@link S3File} data.
|
|
488
|
+
* @param start The starting index.
|
|
489
|
+
* @param end The ending index, exclusive.
|
|
490
|
+
* @param contentType The content-type for the new {@link S3File}.
|
|
491
|
+
*/
|
|
492
|
+
slice(start?: number, end?: number, contentType?: string): S3File;
|
|
474
493
|
/**
|
|
475
494
|
* Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
|
|
476
495
|
*
|
package/dist/index.js
CHANGED
|
@@ -319,32 +319,75 @@ function getAuthorizationHeader(keyCache, method, path, query, date, sortedSigne
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// src/branded.ts
|
|
322
|
-
function ensureValidBucketName(
|
|
323
|
-
if (
|
|
324
|
-
throw new
|
|
322
|
+
function ensureValidBucketName(bucket) {
|
|
323
|
+
if (typeof bucket !== "string") {
|
|
324
|
+
throw new TypeError("`bucket` is required and must be a `string`.");
|
|
325
325
|
}
|
|
326
|
-
if (
|
|
327
|
-
throw new Error("`
|
|
326
|
+
if (bucket.length < 3 || bucket.length > 63) {
|
|
327
|
+
throw new Error("`bucket` must be between 3 and 63 characters long.");
|
|
328
328
|
}
|
|
329
|
-
if (
|
|
329
|
+
if (bucket.startsWith(".") || bucket.endsWith(".")) {
|
|
330
|
+
throw new Error("`bucket` must not start or end with a period (.)");
|
|
331
|
+
}
|
|
332
|
+
if (!/^[a-z0-9.-]+$/.test(bucket)) {
|
|
330
333
|
throw new Error(
|
|
331
|
-
"`
|
|
334
|
+
"`bucket` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
|
|
332
335
|
);
|
|
333
336
|
}
|
|
334
|
-
if (
|
|
335
|
-
throw new Error("`
|
|
337
|
+
if (bucket.includes("..")) {
|
|
338
|
+
throw new Error("`bucket` must not contain two adjacent periods (..)");
|
|
339
|
+
}
|
|
340
|
+
return bucket;
|
|
341
|
+
}
|
|
342
|
+
function ensureValidAccessKeyId(accessKeyId) {
|
|
343
|
+
if (typeof accessKeyId !== "string") {
|
|
344
|
+
throw new TypeError("`AccessKeyId` is required and must be a `string`.");
|
|
345
|
+
}
|
|
346
|
+
if (accessKeyId.length < 1) {
|
|
347
|
+
throw new RangeError("`AccessKeyId` must be at least 1 character long.");
|
|
348
|
+
}
|
|
349
|
+
return accessKeyId;
|
|
350
|
+
}
|
|
351
|
+
function ensureValidSecretAccessKey(secretAccessKey) {
|
|
352
|
+
if (typeof secretAccessKey !== "string") {
|
|
353
|
+
throw new TypeError(
|
|
354
|
+
"`SecretAccessKey` is required and must be a `string`."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (secretAccessKey.length < 1) {
|
|
358
|
+
throw new RangeError(
|
|
359
|
+
"`SecretAccessKey` must be at least 1 character long."
|
|
360
|
+
);
|
|
336
361
|
}
|
|
337
|
-
return
|
|
362
|
+
return secretAccessKey;
|
|
338
363
|
}
|
|
339
364
|
function ensureValidPath(path) {
|
|
340
365
|
if (typeof path !== "string") {
|
|
341
|
-
throw new TypeError("`path` must be a `string`.");
|
|
366
|
+
throw new TypeError("`path` is required and must be a `string`.");
|
|
342
367
|
}
|
|
343
368
|
if (path.length < 1) {
|
|
344
369
|
throw new RangeError("`path` must be at least 1 character long.");
|
|
345
370
|
}
|
|
346
371
|
return path;
|
|
347
372
|
}
|
|
373
|
+
function ensureValidEndpoint(endpoint) {
|
|
374
|
+
if (typeof endpoint !== "string") {
|
|
375
|
+
throw new TypeError("`endpoint` is required and must be a `string`.");
|
|
376
|
+
}
|
|
377
|
+
if (endpoint.length < 1) {
|
|
378
|
+
throw new RangeError("`endpoint` must be at least 1 character long.");
|
|
379
|
+
}
|
|
380
|
+
return endpoint;
|
|
381
|
+
}
|
|
382
|
+
function ensureValidRegion(region) {
|
|
383
|
+
if (typeof region !== "string") {
|
|
384
|
+
throw new TypeError("`region` is required and must be a `string`.");
|
|
385
|
+
}
|
|
386
|
+
if (region.length < 1) {
|
|
387
|
+
throw new RangeError("`region` must be at least 1 character long.");
|
|
388
|
+
}
|
|
389
|
+
return region;
|
|
390
|
+
}
|
|
348
391
|
|
|
349
392
|
// src/assertNever.ts
|
|
350
393
|
function assertNever(v) {
|
|
@@ -373,9 +416,10 @@ function encodeURIComponentExtended(value) {
|
|
|
373
416
|
}
|
|
374
417
|
|
|
375
418
|
// src/S3Client.ts
|
|
376
|
-
var
|
|
377
|
-
var
|
|
378
|
-
var
|
|
419
|
+
var kWrite = Symbol("kWrite");
|
|
420
|
+
var kStream = Symbol("kStream");
|
|
421
|
+
var kSignedRequest = Symbol("kSignedRequest");
|
|
422
|
+
var kGetEffectiveParams = Symbol("kGetEffectiveParams");
|
|
379
423
|
var xmlParser2 = new XMLParser2({
|
|
380
424
|
ignoreAttributes: true,
|
|
381
425
|
isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "ListPartsResult.Part" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
|
|
@@ -387,7 +431,7 @@ var xmlBuilder = new XMLBuilder({
|
|
|
387
431
|
var S3Client = class {
|
|
388
432
|
#options;
|
|
389
433
|
#keyCache = new KeyCache();
|
|
390
|
-
// TODO: pass options to this in client? Do we want to expose
|
|
434
|
+
// TODO: pass options to this in client? Do we want to expose the internal use of undici?
|
|
391
435
|
#dispatcher = new Agent();
|
|
392
436
|
/**
|
|
393
437
|
* Create a new instance of an S3 bucket so that credentials can be managed from a single instance instead of being passed to every method.
|
|
@@ -398,52 +442,23 @@ var S3Client = class {
|
|
|
398
442
|
if (!options) {
|
|
399
443
|
throw new Error("`options` is required.");
|
|
400
444
|
}
|
|
401
|
-
const {
|
|
402
|
-
accessKeyId,
|
|
403
|
-
secretAccessKey,
|
|
404
|
-
endpoint,
|
|
405
|
-
region,
|
|
406
|
-
bucket,
|
|
407
|
-
sessionToken
|
|
408
|
-
} = options;
|
|
409
|
-
if (!accessKeyId || typeof accessKeyId !== "string") {
|
|
410
|
-
throw new Error("`accessKeyId` is required.");
|
|
411
|
-
}
|
|
412
|
-
if (!secretAccessKey || typeof secretAccessKey !== "string") {
|
|
413
|
-
throw new Error("`secretAccessKey` is required.");
|
|
414
|
-
}
|
|
415
|
-
if (!endpoint || typeof endpoint !== "string") {
|
|
416
|
-
throw new Error("`endpoint` is required.");
|
|
417
|
-
}
|
|
418
|
-
if (!region || typeof region !== "string") {
|
|
419
|
-
throw new Error("`region` is required.");
|
|
420
|
-
}
|
|
421
|
-
if (!bucket || typeof bucket !== "string") {
|
|
422
|
-
throw new Error("`bucket` is required.");
|
|
423
|
-
}
|
|
424
445
|
this.#options = {
|
|
425
|
-
accessKeyId,
|
|
426
|
-
secretAccessKey,
|
|
427
|
-
endpoint,
|
|
428
|
-
region,
|
|
429
|
-
bucket,
|
|
430
|
-
sessionToken
|
|
446
|
+
accessKeyId: ensureValidAccessKeyId(options.accessKeyId),
|
|
447
|
+
secretAccessKey: ensureValidSecretAccessKey(options.secretAccessKey),
|
|
448
|
+
endpoint: ensureValidEndpoint(options.endpoint),
|
|
449
|
+
region: ensureValidRegion(options.region),
|
|
450
|
+
bucket: ensureValidBucketName(options.bucket),
|
|
451
|
+
sessionToken: options.sessionToken
|
|
431
452
|
};
|
|
432
453
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
},
|
|
442
|
-
delete: () => {
|
|
443
|
-
// TODO: DeleteBucketCors
|
|
444
|
-
},
|
|
445
|
-
};
|
|
446
|
-
*/
|
|
454
|
+
/** @internal */
|
|
455
|
+
[kGetEffectiveParams](options) {
|
|
456
|
+
return [
|
|
457
|
+
options.region ? ensureValidRegion(options.region) : this.#options.region,
|
|
458
|
+
options.endpoint ? ensureValidEndpoint(options.endpoint) : this.#options.endpoint,
|
|
459
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket
|
|
460
|
+
];
|
|
461
|
+
}
|
|
447
462
|
/**
|
|
448
463
|
* Creates an S3File instance for the given path.
|
|
449
464
|
*
|
|
@@ -511,33 +526,36 @@ var S3Client = class {
|
|
|
511
526
|
* });
|
|
512
527
|
* ```
|
|
513
528
|
*/
|
|
514
|
-
presign(path,
|
|
515
|
-
const contentLength =
|
|
529
|
+
presign(path, options = {}) {
|
|
530
|
+
const contentLength = options.contentLength ?? void 0;
|
|
516
531
|
if (typeof contentLength === "number") {
|
|
517
532
|
if (contentLength < 0) {
|
|
518
533
|
throw new RangeError("`contentLength` must be >= 0.");
|
|
519
534
|
}
|
|
520
535
|
}
|
|
521
|
-
const method =
|
|
522
|
-
const contentType =
|
|
523
|
-
const region
|
|
524
|
-
const
|
|
525
|
-
const endpoint = optio2ns.endpoint ?? this.#options.endpoint;
|
|
526
|
-
const responseOptions = optio2ns.response;
|
|
536
|
+
const method = options.method ?? "GET";
|
|
537
|
+
const contentType = options.type ?? void 0;
|
|
538
|
+
const [region, endpoint, bucket] = this[kGetEffectiveParams](options);
|
|
539
|
+
const responseOptions = options.response;
|
|
527
540
|
const contentDisposition = responseOptions?.contentDisposition;
|
|
528
541
|
const responseContentDisposition = contentDisposition ? getContentDispositionHeader(contentDisposition) : void 0;
|
|
529
|
-
const res = buildRequestUrl(
|
|
542
|
+
const res = buildRequestUrl(
|
|
543
|
+
endpoint,
|
|
544
|
+
bucket,
|
|
545
|
+
region,
|
|
546
|
+
ensureValidPath(path)
|
|
547
|
+
);
|
|
530
548
|
const now2 = /* @__PURE__ */ new Date();
|
|
531
549
|
const date = getAmzDate(now2);
|
|
532
550
|
const query = buildSearchParams(
|
|
533
551
|
`${this.#options.accessKeyId}/${date.date}/${region}/s3/aws4_request`,
|
|
534
552
|
date,
|
|
535
|
-
|
|
553
|
+
options.expiresIn ?? 3600,
|
|
536
554
|
typeof contentLength === "number" || typeof contentType === "string" ? typeof contentLength === "number" && typeof contentType === "string" ? "content-length;content-type;host" : typeof contentLength === "number" ? "content-length;host" : typeof contentType === "string" ? "content-type;host" : "" : "host",
|
|
537
555
|
unsignedPayload,
|
|
538
|
-
|
|
556
|
+
options.storageClass,
|
|
539
557
|
this.#options.sessionToken,
|
|
540
|
-
|
|
558
|
+
options.acl,
|
|
541
559
|
responseContentDisposition
|
|
542
560
|
);
|
|
543
561
|
const dataDigest = typeof contentLength === "number" || typeof contentType === "string" ? createCanonicalDataDigest(
|
|
@@ -573,9 +591,10 @@ var S3Client = class {
|
|
|
573
591
|
}
|
|
574
592
|
//#region multipart uploads
|
|
575
593
|
async createMultipartUpload(key, options = {}) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
594
|
+
const response = await this[kSignedRequest](
|
|
595
|
+
this.#options.region,
|
|
596
|
+
this.#options.endpoint,
|
|
597
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
579
598
|
"POST",
|
|
580
599
|
ensureValidPath(key),
|
|
581
600
|
"uploads=",
|
|
@@ -583,7 +602,6 @@ var S3Client = class {
|
|
|
583
602
|
void 0,
|
|
584
603
|
void 0,
|
|
585
604
|
void 0,
|
|
586
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
587
605
|
options.signal
|
|
588
606
|
);
|
|
589
607
|
if (response.statusCode !== 200) {
|
|
@@ -602,9 +620,6 @@ var S3Client = class {
|
|
|
602
620
|
* @throws {RangeError} If `options.maxKeys` is not between `1` and `1000`.
|
|
603
621
|
*/
|
|
604
622
|
async listMultipartUploads(options = {}) {
|
|
605
|
-
const bucket = ensureValidBucketName(
|
|
606
|
-
options.bucket ?? this.#options.bucket
|
|
607
|
-
);
|
|
608
623
|
let query = "uploads=";
|
|
609
624
|
if (options.delimiter) {
|
|
610
625
|
if (typeof options.delimiter !== "string") {
|
|
@@ -633,7 +648,10 @@ var S3Client = class {
|
|
|
633
648
|
}
|
|
634
649
|
query += `&prefix=${encodeURIComponent(options.prefix)}`;
|
|
635
650
|
}
|
|
636
|
-
const response = await this[
|
|
651
|
+
const response = await this[kSignedRequest](
|
|
652
|
+
this.#options.region,
|
|
653
|
+
this.#options.endpoint,
|
|
654
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
637
655
|
"GET",
|
|
638
656
|
"",
|
|
639
657
|
query,
|
|
@@ -641,7 +659,6 @@ var S3Client = class {
|
|
|
641
659
|
void 0,
|
|
642
660
|
void 0,
|
|
643
661
|
void 0,
|
|
644
|
-
bucket,
|
|
645
662
|
options.signal
|
|
646
663
|
);
|
|
647
664
|
if (response.statusCode !== 200) {
|
|
@@ -684,7 +701,10 @@ var S3Client = class {
|
|
|
684
701
|
if (!uploadId) {
|
|
685
702
|
throw new Error("`uploadId` is required.");
|
|
686
703
|
}
|
|
687
|
-
const response = await this[
|
|
704
|
+
const response = await this[kSignedRequest](
|
|
705
|
+
this.#options.region,
|
|
706
|
+
this.#options.endpoint,
|
|
707
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
688
708
|
"DELETE",
|
|
689
709
|
ensureValidPath(path),
|
|
690
710
|
`uploadId=${encodeURIComponent(uploadId)}`,
|
|
@@ -692,10 +712,9 @@ var S3Client = class {
|
|
|
692
712
|
void 0,
|
|
693
713
|
void 0,
|
|
694
714
|
void 0,
|
|
695
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
696
715
|
options.signal
|
|
697
716
|
);
|
|
698
|
-
if (response.statusCode !== 204) {
|
|
717
|
+
if (response.statusCode !== 204 && response.statusCode !== 200) {
|
|
699
718
|
throw await getResponseError(response, path);
|
|
700
719
|
}
|
|
701
720
|
}
|
|
@@ -716,7 +735,10 @@ var S3Client = class {
|
|
|
716
735
|
}))
|
|
717
736
|
}
|
|
718
737
|
});
|
|
719
|
-
const response = await this[
|
|
738
|
+
const response = await this[kSignedRequest](
|
|
739
|
+
this.#options.region,
|
|
740
|
+
this.#options.endpoint,
|
|
741
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
720
742
|
"POST",
|
|
721
743
|
ensureValidPath(path),
|
|
722
744
|
`uploadId=${encodeURIComponent(uploadId)}`,
|
|
@@ -724,7 +746,6 @@ var S3Client = class {
|
|
|
724
746
|
void 0,
|
|
725
747
|
void 0,
|
|
726
748
|
void 0,
|
|
727
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
728
749
|
options.signal
|
|
729
750
|
);
|
|
730
751
|
if (response.statusCode !== 200) {
|
|
@@ -760,7 +781,10 @@ var S3Client = class {
|
|
|
760
781
|
if (typeof partNumber !== "number" || partNumber <= 0) {
|
|
761
782
|
throw new Error("`partNumber` has to be a `number` which is >= 1.");
|
|
762
783
|
}
|
|
763
|
-
const response = await this[
|
|
784
|
+
const response = await this[kSignedRequest](
|
|
785
|
+
this.#options.region,
|
|
786
|
+
this.#options.endpoint,
|
|
787
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
764
788
|
"PUT",
|
|
765
789
|
ensureValidPath(path),
|
|
766
790
|
`partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
|
|
@@ -768,7 +792,6 @@ var S3Client = class {
|
|
|
768
792
|
void 0,
|
|
769
793
|
void 0,
|
|
770
794
|
void 0,
|
|
771
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
772
795
|
options.signal
|
|
773
796
|
);
|
|
774
797
|
if (response.statusCode === 200) {
|
|
@@ -812,7 +835,10 @@ var S3Client = class {
|
|
|
812
835
|
query += `&part-number-marker=${encodeURIComponent(options.partNumberMarker)}`;
|
|
813
836
|
}
|
|
814
837
|
query += `&uploadId=${encodeURIComponent(uploadId)}`;
|
|
815
|
-
const response = await this[
|
|
838
|
+
const response = await this[kSignedRequest](
|
|
839
|
+
this.#options.region,
|
|
840
|
+
this.#options.endpoint,
|
|
841
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
816
842
|
"GET",
|
|
817
843
|
ensureValidPath(path),
|
|
818
844
|
// 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
|
|
@@ -821,7 +847,6 @@ var S3Client = class {
|
|
|
821
847
|
void 0,
|
|
822
848
|
void 0,
|
|
823
849
|
void 0,
|
|
824
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
825
850
|
options?.signal
|
|
826
851
|
);
|
|
827
852
|
if (response.statusCode === 200) {
|
|
@@ -864,7 +889,7 @@ var S3Client = class {
|
|
|
864
889
|
* @throws {S3Error} If the bucket could not be created, e.g. if it already exists.
|
|
865
890
|
* @remarks Uses [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html)
|
|
866
891
|
*/
|
|
867
|
-
async createBucket(name, options) {
|
|
892
|
+
async createBucket(name, options = {}) {
|
|
868
893
|
let body;
|
|
869
894
|
if (options) {
|
|
870
895
|
const location = options.location && (options.location.name || options.location.type) ? {
|
|
@@ -885,7 +910,10 @@ var S3Client = class {
|
|
|
885
910
|
}) : void 0;
|
|
886
911
|
}
|
|
887
912
|
const additionalSignedHeaders = body ? { "content-md5": md5Base64(body) } : void 0;
|
|
888
|
-
const response = await this[
|
|
913
|
+
const response = await this[kSignedRequest](
|
|
914
|
+
options.region ? ensureValidRegion(options.region) : this.#options.region,
|
|
915
|
+
options.endpoint ? ensureValidEndpoint(options.endpoint) : this.#options.endpoint,
|
|
916
|
+
ensureValidBucketName(name),
|
|
889
917
|
"PUT",
|
|
890
918
|
"",
|
|
891
919
|
void 0,
|
|
@@ -893,8 +921,7 @@ var S3Client = class {
|
|
|
893
921
|
additionalSignedHeaders,
|
|
894
922
|
void 0,
|
|
895
923
|
void 0,
|
|
896
|
-
|
|
897
|
-
options?.signal
|
|
924
|
+
options.signal
|
|
898
925
|
);
|
|
899
926
|
if (400 <= response.statusCode && response.statusCode < 500) {
|
|
900
927
|
throw await getResponseError(response, "");
|
|
@@ -913,7 +940,10 @@ var S3Client = class {
|
|
|
913
940
|
* @remarks Uses [`DeleteBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html).
|
|
914
941
|
*/
|
|
915
942
|
async deleteBucket(name, options) {
|
|
916
|
-
const response = await this[
|
|
943
|
+
const response = await this[kSignedRequest](
|
|
944
|
+
this.#options.region,
|
|
945
|
+
this.#options.endpoint,
|
|
946
|
+
ensureValidBucketName(name),
|
|
917
947
|
"DELETE",
|
|
918
948
|
"",
|
|
919
949
|
void 0,
|
|
@@ -921,7 +951,6 @@ var S3Client = class {
|
|
|
921
951
|
void 0,
|
|
922
952
|
void 0,
|
|
923
953
|
void 0,
|
|
924
|
-
ensureValidBucketName(name),
|
|
925
954
|
options?.signal
|
|
926
955
|
);
|
|
927
956
|
if (400 <= response.statusCode && response.statusCode < 500) {
|
|
@@ -940,7 +969,10 @@ var S3Client = class {
|
|
|
940
969
|
* @remarks Uses [`HeadBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html).
|
|
941
970
|
*/
|
|
942
971
|
async bucketExists(name, options) {
|
|
943
|
-
const response = await this[
|
|
972
|
+
const response = await this[kSignedRequest](
|
|
973
|
+
this.#options.region,
|
|
974
|
+
this.#options.endpoint,
|
|
975
|
+
ensureValidBucketName(name),
|
|
944
976
|
"HEAD",
|
|
945
977
|
"",
|
|
946
978
|
void 0,
|
|
@@ -948,7 +980,6 @@ var S3Client = class {
|
|
|
948
980
|
void 0,
|
|
949
981
|
void 0,
|
|
950
982
|
void 0,
|
|
951
|
-
ensureValidBucketName(name),
|
|
952
983
|
options?.signal
|
|
953
984
|
);
|
|
954
985
|
if (response.statusCode !== 404 && 400 <= response.statusCode && response.statusCode < 500) {
|
|
@@ -979,7 +1010,10 @@ var S3Client = class {
|
|
|
979
1010
|
}))
|
|
980
1011
|
}
|
|
981
1012
|
});
|
|
982
|
-
const response = await this[
|
|
1013
|
+
const response = await this[kSignedRequest](
|
|
1014
|
+
this.#options.region,
|
|
1015
|
+
this.#options.endpoint,
|
|
1016
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
983
1017
|
"PUT",
|
|
984
1018
|
"",
|
|
985
1019
|
"cors=",
|
|
@@ -990,7 +1024,6 @@ var S3Client = class {
|
|
|
990
1024
|
},
|
|
991
1025
|
void 0,
|
|
992
1026
|
void 0,
|
|
993
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
994
1027
|
options.signal
|
|
995
1028
|
);
|
|
996
1029
|
if (response.statusCode === 200) {
|
|
@@ -1008,7 +1041,10 @@ var S3Client = class {
|
|
|
1008
1041
|
* @remarks Uses [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html).
|
|
1009
1042
|
*/
|
|
1010
1043
|
async getBucketCors(options = {}) {
|
|
1011
|
-
const response = await this[
|
|
1044
|
+
const response = await this[kSignedRequest](
|
|
1045
|
+
this.#options.region,
|
|
1046
|
+
this.#options.endpoint,
|
|
1047
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
1012
1048
|
"GET",
|
|
1013
1049
|
"",
|
|
1014
1050
|
"cors=",
|
|
@@ -1017,7 +1053,6 @@ var S3Client = class {
|
|
|
1017
1053
|
void 0,
|
|
1018
1054
|
void 0,
|
|
1019
1055
|
void 0,
|
|
1020
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
1021
1056
|
options.signal
|
|
1022
1057
|
);
|
|
1023
1058
|
if (response.statusCode !== 200) {
|
|
@@ -1030,7 +1065,10 @@ var S3Client = class {
|
|
|
1030
1065
|
* @remarks Uses [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html).
|
|
1031
1066
|
*/
|
|
1032
1067
|
async deleteBucketCors(options = {}) {
|
|
1033
|
-
const response = await this[
|
|
1068
|
+
const response = await this[kSignedRequest](
|
|
1069
|
+
this.#options.region,
|
|
1070
|
+
this.#options.endpoint,
|
|
1071
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
1034
1072
|
"DELETE",
|
|
1035
1073
|
"",
|
|
1036
1074
|
"cors=",
|
|
@@ -1039,7 +1077,6 @@ var S3Client = class {
|
|
|
1039
1077
|
void 0,
|
|
1040
1078
|
void 0,
|
|
1041
1079
|
void 0,
|
|
1042
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
1043
1080
|
options.signal
|
|
1044
1081
|
);
|
|
1045
1082
|
if (response.statusCode !== 204) {
|
|
@@ -1109,7 +1146,10 @@ var S3Client = class {
|
|
|
1109
1146
|
}
|
|
1110
1147
|
query += `&start-after=${encodeURIComponent(options.startAfter)}`;
|
|
1111
1148
|
}
|
|
1112
|
-
const response = await this[
|
|
1149
|
+
const response = await this[kSignedRequest](
|
|
1150
|
+
ensureValidRegion(this.#options.region),
|
|
1151
|
+
ensureValidEndpoint(this.#options.endpoint),
|
|
1152
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
1113
1153
|
"GET",
|
|
1114
1154
|
"",
|
|
1115
1155
|
query,
|
|
@@ -1117,7 +1157,6 @@ var S3Client = class {
|
|
|
1117
1157
|
void 0,
|
|
1118
1158
|
void 0,
|
|
1119
1159
|
void 0,
|
|
1120
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
1121
1160
|
options.signal
|
|
1122
1161
|
);
|
|
1123
1162
|
if (response.statusCode !== 200) {
|
|
@@ -1158,7 +1197,10 @@ var S3Client = class {
|
|
|
1158
1197
|
}))
|
|
1159
1198
|
}
|
|
1160
1199
|
});
|
|
1161
|
-
const response = await this[
|
|
1200
|
+
const response = await this[kSignedRequest](
|
|
1201
|
+
this.#options.region,
|
|
1202
|
+
this.#options.endpoint,
|
|
1203
|
+
options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
|
|
1162
1204
|
"POST",
|
|
1163
1205
|
"",
|
|
1164
1206
|
"delete=",
|
|
@@ -1169,7 +1211,6 @@ var S3Client = class {
|
|
|
1169
1211
|
},
|
|
1170
1212
|
void 0,
|
|
1171
1213
|
void 0,
|
|
1172
|
-
ensureValidBucketName(options.bucket ?? this.#options.bucket),
|
|
1173
1214
|
options.signal
|
|
1174
1215
|
);
|
|
1175
1216
|
if (response.statusCode === 200) {
|
|
@@ -1207,16 +1248,8 @@ var S3Client = class {
|
|
|
1207
1248
|
* TODO: Maybe move this into a separate free function?
|
|
1208
1249
|
* @internal
|
|
1209
1250
|
*/
|
|
1210
|
-
async [
|
|
1211
|
-
const
|
|
1212
|
-
const region = this.#options.region;
|
|
1213
|
-
const effectiveBucket = bucket ?? this.#options.bucket;
|
|
1214
|
-
const url = buildRequestUrl(
|
|
1215
|
-
endpoint,
|
|
1216
|
-
effectiveBucket,
|
|
1217
|
-
region,
|
|
1218
|
-
pathWithoutBucket
|
|
1219
|
-
);
|
|
1251
|
+
async [kSignedRequest](region, endpoint, bucket, method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, signal) {
|
|
1252
|
+
const url = buildRequestUrl(endpoint, bucket, region, pathWithoutBucket);
|
|
1220
1253
|
if (query) {
|
|
1221
1254
|
url.search = query;
|
|
1222
1255
|
}
|
|
@@ -1264,7 +1297,7 @@ var S3Client = class {
|
|
|
1264
1297
|
* @internal
|
|
1265
1298
|
* @param {import("./index.d.ts").UndiciBodyInit} data TODO
|
|
1266
1299
|
*/
|
|
1267
|
-
async [
|
|
1300
|
+
async [kWrite](path, data, contentType, contentLength, contentHash, rageStart, rangeEndExclusive, signal = void 0) {
|
|
1268
1301
|
const bucket = this.#options.bucket;
|
|
1269
1302
|
const endpoint = this.#options.endpoint;
|
|
1270
1303
|
const region = this.#options.region;
|
|
@@ -1320,7 +1353,7 @@ var S3Client = class {
|
|
|
1320
1353
|
/**
|
|
1321
1354
|
* @internal
|
|
1322
1355
|
*/
|
|
1323
|
-
[
|
|
1356
|
+
[kStream](path, contentHash, rageStart, rangeEndExclusive) {
|
|
1324
1357
|
const bucket = this.#options.bucket;
|
|
1325
1358
|
const endpoint = this.#options.endpoint;
|
|
1326
1359
|
const region = this.#options.region;
|
|
@@ -1505,7 +1538,12 @@ var S3File = class _S3File {
|
|
|
1505
1538
|
this.#end = end;
|
|
1506
1539
|
this.#contentType = contentType ?? "application/octet-stream";
|
|
1507
1540
|
}
|
|
1508
|
-
|
|
1541
|
+
/**
|
|
1542
|
+
* Creates and returns a new {@link S3File} containing a subset of this {@link S3File} data.
|
|
1543
|
+
* @param start The starting index.
|
|
1544
|
+
* @param end The ending index, exclusive.
|
|
1545
|
+
* @param contentType The content-type for the new {@link S3File}.
|
|
1546
|
+
*/
|
|
1509
1547
|
slice(start, end, contentType) {
|
|
1510
1548
|
return new _S3File(
|
|
1511
1549
|
this.#client,
|
|
@@ -1523,7 +1561,11 @@ var S3File = class _S3File {
|
|
|
1523
1561
|
* @throws {Error} If the server returns an invalid response.
|
|
1524
1562
|
*/
|
|
1525
1563
|
async stat(options = {}) {
|
|
1526
|
-
const
|
|
1564
|
+
const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
|
|
1565
|
+
const response = await this.#client[kSignedRequest](
|
|
1566
|
+
region,
|
|
1567
|
+
endpoint,
|
|
1568
|
+
bucket,
|
|
1527
1569
|
"HEAD",
|
|
1528
1570
|
this.#path,
|
|
1529
1571
|
void 0,
|
|
@@ -1531,7 +1573,6 @@ var S3File = class _S3File {
|
|
|
1531
1573
|
void 0,
|
|
1532
1574
|
void 0,
|
|
1533
1575
|
void 0,
|
|
1534
|
-
void 0,
|
|
1535
1576
|
options.signal
|
|
1536
1577
|
);
|
|
1537
1578
|
response.body.dump();
|
|
@@ -1554,7 +1595,11 @@ var S3File = class _S3File {
|
|
|
1554
1595
|
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
1555
1596
|
*/
|
|
1556
1597
|
async exists(options = {}) {
|
|
1557
|
-
const
|
|
1598
|
+
const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
|
|
1599
|
+
const response = await this.#client[kSignedRequest](
|
|
1600
|
+
region,
|
|
1601
|
+
endpoint,
|
|
1602
|
+
bucket,
|
|
1558
1603
|
"HEAD",
|
|
1559
1604
|
this.#path,
|
|
1560
1605
|
void 0,
|
|
@@ -1562,7 +1607,6 @@ var S3File = class _S3File {
|
|
|
1562
1607
|
void 0,
|
|
1563
1608
|
void 0,
|
|
1564
1609
|
void 0,
|
|
1565
|
-
void 0,
|
|
1566
1610
|
options.signal
|
|
1567
1611
|
);
|
|
1568
1612
|
response.body.dump();
|
|
@@ -1599,7 +1643,11 @@ var S3File = class _S3File {
|
|
|
1599
1643
|
* ```
|
|
1600
1644
|
*/
|
|
1601
1645
|
async delete(options = {}) {
|
|
1602
|
-
const
|
|
1646
|
+
const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
|
|
1647
|
+
const response = await this.#client[kSignedRequest](
|
|
1648
|
+
region,
|
|
1649
|
+
endpoint,
|
|
1650
|
+
bucket,
|
|
1603
1651
|
"DELETE",
|
|
1604
1652
|
this.#path,
|
|
1605
1653
|
void 0,
|
|
@@ -1607,7 +1655,6 @@ var S3File = class _S3File {
|
|
|
1607
1655
|
void 0,
|
|
1608
1656
|
void 0,
|
|
1609
1657
|
void 0,
|
|
1610
|
-
void 0,
|
|
1611
1658
|
options.signal
|
|
1612
1659
|
);
|
|
1613
1660
|
if (response.statusCode === 204) {
|
|
@@ -1638,7 +1685,7 @@ var S3File = class _S3File {
|
|
|
1638
1685
|
}
|
|
1639
1686
|
/** @returns {ReadableStream<Uint8Array>} */
|
|
1640
1687
|
stream() {
|
|
1641
|
-
return this.#client[
|
|
1688
|
+
return this.#client[kStream](this.#path, void 0, this.#start, this.#end);
|
|
1642
1689
|
}
|
|
1643
1690
|
async #transformData(data) {
|
|
1644
1691
|
if (typeof data === "string") {
|
|
@@ -1681,7 +1728,7 @@ var S3File = class _S3File {
|
|
|
1681
1728
|
async write(data, options = {}) {
|
|
1682
1729
|
const signal = void 0;
|
|
1683
1730
|
const [bytes, length, hash] = await this.#transformData(data);
|
|
1684
|
-
return await this.#client[
|
|
1731
|
+
return await this.#client[kWrite](
|
|
1685
1732
|
this.#path,
|
|
1686
1733
|
bytes,
|
|
1687
1734
|
options.type ?? this.#contentType,
|
|
@@ -1692,15 +1739,6 @@ var S3File = class _S3File {
|
|
|
1692
1739
|
signal
|
|
1693
1740
|
);
|
|
1694
1741
|
}
|
|
1695
|
-
/*
|
|
1696
|
-
// Future API?
|
|
1697
|
-
setTags(): Promise<void> {
|
|
1698
|
-
throw new Error("Not implemented");
|
|
1699
|
-
}
|
|
1700
|
-
getTags(): Promise<unknown> {
|
|
1701
|
-
throw new Error("Not implemented");
|
|
1702
|
-
}
|
|
1703
|
-
*/
|
|
1704
1742
|
};
|
|
1705
1743
|
export {
|
|
1706
1744
|
S3BucketEntry,
|
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.7.
|
|
5
|
+
"version": "0.7.5",
|
|
6
6
|
"description": "A server-side S3 API for the regular user.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"s3",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"type": "module",
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup",
|
|
37
|
-
"test": "tsgo && tsx --test src/*.test.ts",
|
|
38
|
-
"test:integration": "tsgo && tsx --test src/test.integration.ts",
|
|
37
|
+
"test": "tsgo && tsx --test src/*.test.ts src/test/*.test.ts",
|
|
38
|
+
"test:integration": "tsgo && tsx --test src/test/test.integration.ts",
|
|
39
39
|
"ci": "biome ci ./src",
|
|
40
40
|
"docs": "typedoc",
|
|
41
41
|
"lint": "biome lint ./src",
|
|
@@ -47,13 +47,14 @@
|
|
|
47
47
|
"undici": "^7.11.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@biomejs/biome": "2.
|
|
51
|
-
"@testcontainers/localstack": "^11.2.
|
|
52
|
-
"@testcontainers/minio": "^11.2.
|
|
50
|
+
"@biomejs/biome": "2.1.1",
|
|
51
|
+
"@testcontainers/localstack": "^11.2.1",
|
|
52
|
+
"@testcontainers/minio": "^11.2.1",
|
|
53
53
|
"@types/node": "^24.0.10",
|
|
54
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
54
|
+
"@typescript/native-preview": "^7.0.0-dev.20250708.1",
|
|
55
55
|
"expect": "^30.0.4",
|
|
56
|
-
"lefthook": "^1.
|
|
56
|
+
"lefthook": "^1.12.0",
|
|
57
|
+
"testcontainers": "^11.2.1",
|
|
57
58
|
"tsup": "^8.5.0",
|
|
58
59
|
"tsx": "^4.20.3",
|
|
59
60
|
"typedoc": "^0.28.7"
|