s3mini 0.9.3 → 0.9.4
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 +9 -1
- package/dist/s3mini.d.ts +10 -0
- package/dist/s3mini.js +246 -68
- package/dist/s3mini.js.map +1 -1
- package/dist/s3mini.min.js +1 -1
- package/dist/s3mini.min.js.map +1 -1
- package/package.json +13 -9
- package/src/S3.ts +264 -70
- package/src/types.ts +54 -0
- package/src/utils.ts +17 -0
package/README.md
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
`s3mini` is an ultra-lightweight Typescript client (~20 KB minified, ≈15 % more ops/s) for S3-compatible object storage. It runs on Node, Bun, Cloudflare Workers, and other edge platforms. It has been tested on Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Ceph, Oracle, Garage and MinIO. (No Browser support!)
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
5
9
|
[[github](https://github.com/good-lly/s3mini)]
|
|
6
10
|
[[issues](https://github.com/good-lly/s3mini/issues)]
|
|
7
11
|
[[npm](https://www.npmjs.com/package/s3mini)]
|
|
8
12
|
|
|
9
13
|
## Features
|
|
10
14
|
|
|
11
|
-
- 🚀 Light and fast:
|
|
15
|
+
- 🚀 Light and fast: ~20 KB (minified, not gzipped), up to 1.37x faster on Bun vs Node.
|
|
12
16
|
- 🔧 Zero dependencies; supports AWS SigV4, pre-signed URLs, and SSE-C headers (tested on Cloudflare)
|
|
13
17
|
- 🟠 Works on Cloudflare Workers; ideal for edge computing, Node, and Bun (no browser support).
|
|
14
18
|
- 🔑 Only the essential S3 APIs—improved list, put, get, delete, and a few more.
|
|
@@ -44,6 +48,10 @@ Dev:
|
|
|
44
48
|
|
|
45
49
|
<a href="https://github.com/good-lly/s3mini/issues/"> <img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg" alt="Contributions welcome" /></a>
|
|
46
50
|
|
|
51
|
+
### Bun vs Node
|
|
52
|
+
|
|
53
|
+
s3mini is tested on both Node and Bun. In our benchmarks against MinIO, Bun is roughly **~1.4x faster** on most operations (median across ~40 tests). Blob multipart uploads see the largest gain (~20x) thanks to Bun's native `Blob.slice()`. Results are approximate and will vary by environment.
|
|
54
|
+
|
|
47
55
|
## Table of Contents
|
|
48
56
|
|
|
49
57
|
- [Installation](#installation)
|
package/dist/s3mini.d.ts
CHANGED
|
@@ -164,6 +164,7 @@ declare class S3mini {
|
|
|
164
164
|
readonly logger?: Logger;
|
|
165
165
|
readonly _fetch: typeof fetch;
|
|
166
166
|
readonly minPartSize: number;
|
|
167
|
+
private readonly _bun?;
|
|
167
168
|
private signingKeyDate?;
|
|
168
169
|
private signingKey?;
|
|
169
170
|
constructor({ accessKeyId, secretAccessKey, endpoint, region, requestSizeInBytes, requestAbortTimeout, logger, fetch, minPartSize, }: S3Config);
|
|
@@ -175,6 +176,8 @@ declare class S3mini {
|
|
|
175
176
|
* @returns true if both accessKeyId and secretAccessKey are non-empty.
|
|
176
177
|
*/
|
|
177
178
|
private _hasCredentials;
|
|
179
|
+
/** Run a read op via Bun-native S3, returning null on NoSuchKey. */
|
|
180
|
+
private _bunRead;
|
|
178
181
|
private _ensureValidUrl;
|
|
179
182
|
private _validateMethodIsGetOrHead;
|
|
180
183
|
private _checkKey;
|
|
@@ -249,6 +252,10 @@ declare class S3mini {
|
|
|
249
252
|
private _parseListObjectsResponse;
|
|
250
253
|
private _extractObjectsFromResponse;
|
|
251
254
|
private _extractContinuationToken;
|
|
255
|
+
private _bunListAll;
|
|
256
|
+
private _bunFetchPage;
|
|
257
|
+
private _bunNextCursor;
|
|
258
|
+
private _bunMapListResult;
|
|
252
259
|
/**
|
|
253
260
|
* Lists multipart uploads in the bucket.
|
|
254
261
|
* This method sends a request to list multipart uploads in the specified bucket.
|
|
@@ -579,6 +586,9 @@ declare class S3mini {
|
|
|
579
586
|
*/
|
|
580
587
|
deleteObject(key: string): Promise<boolean>;
|
|
581
588
|
private _deleteObjectsProcess;
|
|
589
|
+
private _sendDeleteRequest;
|
|
590
|
+
private _markDeletedKeys;
|
|
591
|
+
private _logDeleteErrors;
|
|
582
592
|
/**
|
|
583
593
|
* Deletes multiple objects from the bucket.
|
|
584
594
|
* @param {string[]} keys - An array of object keys to delete.
|
package/dist/s3mini.js
CHANGED
|
@@ -31,6 +31,21 @@ const ERROR_UPLOAD_ID_REQUIRED = `${ERROR_PREFIX}uploadId must be a non-empty st
|
|
|
31
31
|
const ERROR_PREFIX_TYPE = `${ERROR_PREFIX}prefix must be a string`;
|
|
32
32
|
const ERROR_DELIMITER_REQUIRED = `${ERROR_PREFIX}delimiter must be a string`;
|
|
33
33
|
|
|
34
|
+
const isBun = typeof navigator !== 'undefined' && navigator.userAgent === 'Bun';
|
|
35
|
+
/** Strips the bucket name from a full endpoint URL, returning the base origin for Bun.S3Client. */
|
|
36
|
+
const extractBaseEndpoint = (endpoint, bucket) => {
|
|
37
|
+
// Path-style (/bucket/…): just use the origin
|
|
38
|
+
if (endpoint.pathname.split('/').some(Boolean)) {
|
|
39
|
+
return endpoint.origin;
|
|
40
|
+
}
|
|
41
|
+
// Virtual-hosted (bucket.host…): strip the bucket subdomain
|
|
42
|
+
const prefix = bucket + '.';
|
|
43
|
+
if (endpoint.hostname.startsWith(prefix)) {
|
|
44
|
+
const base = endpoint.hostname.slice(prefix.length);
|
|
45
|
+
return `${endpoint.protocol}//${base}${endpoint.port ? ':' + endpoint.port : ''}`;
|
|
46
|
+
}
|
|
47
|
+
return endpoint.origin;
|
|
48
|
+
};
|
|
34
49
|
const ENCODR = new TextEncoder();
|
|
35
50
|
const chunkSize = 0x8000; // 32KB chunks
|
|
36
51
|
const HEX_CHARS = new Uint8Array([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102]);
|
|
@@ -398,6 +413,7 @@ class S3mini {
|
|
|
398
413
|
logger;
|
|
399
414
|
_fetch;
|
|
400
415
|
minPartSize;
|
|
416
|
+
_bun;
|
|
401
417
|
signingKeyDate;
|
|
402
418
|
signingKey;
|
|
403
419
|
constructor({ accessKeyId, secretAccessKey, endpoint, region = 'auto', requestSizeInBytes = DEFAULT_REQUEST_SIZE_IN_BYTES, requestAbortTimeout = undefined, logger = undefined, fetch = globalThis.fetch, minPartSize = MIN_PART_SIZE, }) {
|
|
@@ -412,6 +428,16 @@ class S3mini {
|
|
|
412
428
|
this.logger = logger;
|
|
413
429
|
this._fetch = (input, init) => fetch(input, init);
|
|
414
430
|
this.minPartSize = minPartSize;
|
|
431
|
+
if (isBun) {
|
|
432
|
+
const { S3Client } = globalThis.Bun;
|
|
433
|
+
this._bun = new S3Client({
|
|
434
|
+
accessKeyId,
|
|
435
|
+
secretAccessKey,
|
|
436
|
+
endpoint: extractBaseEndpoint(this.endpoint, this.bucketName),
|
|
437
|
+
region: this.region,
|
|
438
|
+
bucket: this.bucketName,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
415
441
|
}
|
|
416
442
|
_sanitize(obj) {
|
|
417
443
|
if (typeof obj !== 'object' || obj === null) {
|
|
@@ -472,6 +498,18 @@ class S3mini {
|
|
|
472
498
|
_hasCredentials() {
|
|
473
499
|
return this.#accessKeyId.trim().length > 0 && this.#secretAccessKey.trim().length > 0;
|
|
474
500
|
}
|
|
501
|
+
/** Run a read op via Bun-native S3, returning null on NoSuchKey. */
|
|
502
|
+
async _bunRead(key, op) {
|
|
503
|
+
try {
|
|
504
|
+
return await op(this._bun.file(key));
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
if (e?.code === 'NoSuchKey') {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
throw e;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
475
513
|
_ensureValidUrl(raw) {
|
|
476
514
|
const candidate = /^(https?:)?\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
477
515
|
try {
|
|
@@ -677,32 +715,24 @@ class S3mini {
|
|
|
677
715
|
}
|
|
678
716
|
_extractBucketName() {
|
|
679
717
|
const url = this.endpoint;
|
|
680
|
-
//
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
718
|
+
// Path-style: bucket is the first non-empty path segment
|
|
719
|
+
const firstSegment = url.pathname.split('/').find(Boolean);
|
|
720
|
+
if (firstSegment) {
|
|
721
|
+
return firstSegment;
|
|
722
|
+
}
|
|
723
|
+
// Virtual-hosted style: bucket is the first subdomain label
|
|
724
|
+
const hostname = url.hostname;
|
|
725
|
+
// IP addresses (v4: digits+dots, v6: contains colons) can't carry a bucket subdomain
|
|
726
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.includes(':')) {
|
|
727
|
+
return '';
|
|
686
728
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// bucket-name.region.digitaloceanspaces.com
|
|
693
|
-
// bucket-name.region.cdn.digitaloceanspaces.com
|
|
694
|
-
if (hostParts.length >= 3) {
|
|
695
|
-
// Check if it's a known S3-compatible service
|
|
696
|
-
const domain = hostParts.slice(-2).join('.');
|
|
697
|
-
const knownDomains = ['amazonaws.com', 'digitaloceanspaces.com', 'cloudflare.com'];
|
|
698
|
-
if (knownDomains.some(d => domain.includes(d))) {
|
|
699
|
-
if (typeof hostParts[0] === 'string') {
|
|
700
|
-
return hostParts[0];
|
|
701
|
-
}
|
|
702
|
-
}
|
|
729
|
+
const labels = hostname.split('.');
|
|
730
|
+
// Need ≥3 labels for virtual-hosted (bucket.service.tld)
|
|
731
|
+
// Single-label (localhost) or two-label (example.com) have no room for a bucket subdomain
|
|
732
|
+
if (labels.length < 3) {
|
|
733
|
+
return '';
|
|
703
734
|
}
|
|
704
|
-
|
|
705
|
-
return hostParts[0] || '';
|
|
735
|
+
return labels[0];
|
|
706
736
|
}
|
|
707
737
|
/**
|
|
708
738
|
* Checks if a bucket exists.
|
|
@@ -732,6 +762,12 @@ class S3mini {
|
|
|
732
762
|
this._checkDelimiter(delimiter);
|
|
733
763
|
this._checkPrefix(prefix);
|
|
734
764
|
this._checkOpts(opts);
|
|
765
|
+
if (this._bun && delimiter === '/') {
|
|
766
|
+
const extraKeys = Object.keys(opts).filter(k => k !== 'delimiter');
|
|
767
|
+
if (extraKeys.length === 0) {
|
|
768
|
+
return this._bunListAll(prefix, maxKeys, opts.delimiter);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
735
771
|
const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
|
|
736
772
|
const unlimited = !(maxKeys && maxKeys > 0);
|
|
737
773
|
let remaining = unlimited ? Infinity : maxKeys;
|
|
@@ -877,6 +913,71 @@ class S3mini {
|
|
|
877
913
|
response.NextMarker ||
|
|
878
914
|
response.nextMarker);
|
|
879
915
|
}
|
|
916
|
+
async _bunListAll(prefix, maxKeys, delimiter) {
|
|
917
|
+
const unlimited = !(maxKeys && maxKeys > 0);
|
|
918
|
+
let remaining = unlimited ? Infinity : maxKeys;
|
|
919
|
+
let startAfter;
|
|
920
|
+
const all = [];
|
|
921
|
+
try {
|
|
922
|
+
do {
|
|
923
|
+
const batchSize = Math.min(remaining === Infinity ? 1000 : remaining, 1000);
|
|
924
|
+
const res = await this._bunFetchPage(prefix, delimiter, batchSize, startAfter);
|
|
925
|
+
const mapped = this._bunMapListResult(res);
|
|
926
|
+
all.push(...mapped);
|
|
927
|
+
if (!unlimited) {
|
|
928
|
+
remaining -= mapped.length;
|
|
929
|
+
}
|
|
930
|
+
startAfter = this._bunNextCursor(res);
|
|
931
|
+
} while (startAfter && remaining > 0);
|
|
932
|
+
}
|
|
933
|
+
catch (e) {
|
|
934
|
+
if (e?.code === 'NoSuchBucket') {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
throw e;
|
|
938
|
+
}
|
|
939
|
+
return all;
|
|
940
|
+
}
|
|
941
|
+
_bunFetchPage(prefix, delimiter, maxKeys, startAfter) {
|
|
942
|
+
return this._bun.list({
|
|
943
|
+
prefix: prefix || undefined,
|
|
944
|
+
delimiter: delimiter || '/',
|
|
945
|
+
maxKeys,
|
|
946
|
+
...(startAfter ? { startAfter } : {}),
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
_bunNextCursor(res) {
|
|
950
|
+
if (!res.isTruncated) {
|
|
951
|
+
return undefined;
|
|
952
|
+
}
|
|
953
|
+
return res.contents?.length ? res.contents.at(-1).key : undefined;
|
|
954
|
+
}
|
|
955
|
+
_bunMapListResult(res) {
|
|
956
|
+
const objects = [];
|
|
957
|
+
if (res.contents) {
|
|
958
|
+
for (const item of res.contents) {
|
|
959
|
+
objects.push({
|
|
960
|
+
Key: item.key,
|
|
961
|
+
Size: item.size,
|
|
962
|
+
LastModified: item.lastModified instanceof Date ? item.lastModified : new Date(item.lastModified),
|
|
963
|
+
ETag: item.etag ?? '',
|
|
964
|
+
StorageClass: item.storageClass ?? '',
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (res.commonPrefixes) {
|
|
969
|
+
for (const item of res.commonPrefixes) {
|
|
970
|
+
objects.push({
|
|
971
|
+
Key: item.prefix,
|
|
972
|
+
Size: 0,
|
|
973
|
+
LastModified: new Date(0),
|
|
974
|
+
ETag: '',
|
|
975
|
+
StorageClass: '',
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return objects;
|
|
980
|
+
}
|
|
880
981
|
/**
|
|
881
982
|
* Lists multipart uploads in the bucket.
|
|
882
983
|
* This method sends a request to list multipart uploads in the specified bucket.
|
|
@@ -923,7 +1024,9 @@ class S3mini {
|
|
|
923
1024
|
* @returns A promise that resolves to the object data (string) or null if not found.
|
|
924
1025
|
*/
|
|
925
1026
|
async getObject(key, opts = {}, ssecHeaders) {
|
|
926
|
-
|
|
1027
|
+
if (this._bun && !ssecHeaders && !Object.keys(opts).length) {
|
|
1028
|
+
return this._bunRead(key, f => f.text());
|
|
1029
|
+
}
|
|
927
1030
|
const res = await this._signedRequest('GET', key, {
|
|
928
1031
|
query: opts, // use opts.query if it exists, otherwise use an empty object
|
|
929
1032
|
tolerated: [200, 404, 412, 304],
|
|
@@ -965,6 +1068,9 @@ class S3mini {
|
|
|
965
1068
|
* @returns A promise that resolves to the object data as an ArrayBuffer or null if not found.
|
|
966
1069
|
*/
|
|
967
1070
|
async getObjectArrayBuffer(key, opts = {}, ssecHeaders) {
|
|
1071
|
+
if (this._bun && !ssecHeaders && !Object.keys(opts).length) {
|
|
1072
|
+
return this._bunRead(key, f => f.arrayBuffer());
|
|
1073
|
+
}
|
|
968
1074
|
const res = await this._signedRequest('GET', key, {
|
|
969
1075
|
query: opts,
|
|
970
1076
|
tolerated: [200, 404, 412, 304],
|
|
@@ -985,6 +1091,9 @@ class S3mini {
|
|
|
985
1091
|
* @returns A promise that resolves to the object data as JSON or null if not found.
|
|
986
1092
|
*/
|
|
987
1093
|
async getObjectJSON(key, opts = {}, ssecHeaders) {
|
|
1094
|
+
if (this._bun && !ssecHeaders && !Object.keys(opts).length) {
|
|
1095
|
+
return this._bunRead(key, f => f.json());
|
|
1096
|
+
}
|
|
988
1097
|
const res = await this._signedRequest('GET', key, {
|
|
989
1098
|
query: opts,
|
|
990
1099
|
tolerated: [200, 404, 412, 304],
|
|
@@ -1005,6 +1114,19 @@ class S3mini {
|
|
|
1005
1114
|
* @returns A promise that resolves to an object containing the ETag and the object data as an ArrayBuffer or null if not found.
|
|
1006
1115
|
*/
|
|
1007
1116
|
async getObjectWithETag(key, opts = {}, ssecHeaders) {
|
|
1117
|
+
if (this._bun && !ssecHeaders && !Object.keys(opts).length) {
|
|
1118
|
+
try {
|
|
1119
|
+
const f = this._bun.file(key);
|
|
1120
|
+
const [stat, data] = await Promise.all([f.stat(), f.arrayBuffer()]);
|
|
1121
|
+
return { etag: sanitizeETag(stat.etag), data };
|
|
1122
|
+
}
|
|
1123
|
+
catch (e) {
|
|
1124
|
+
if (e?.code === 'NoSuchKey') {
|
|
1125
|
+
return { etag: null, data: null };
|
|
1126
|
+
}
|
|
1127
|
+
throw e;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1008
1130
|
try {
|
|
1009
1131
|
const res = await this._signedRequest('GET', key, {
|
|
1010
1132
|
query: opts,
|
|
@@ -1039,6 +1161,28 @@ class S3mini {
|
|
|
1039
1161
|
* @returns A promise that resolves to the Response object.
|
|
1040
1162
|
*/
|
|
1041
1163
|
async getObjectRaw(key, wholeFile = true, rangeFrom = 0, rangeTo, opts = {}, ssecHeaders) {
|
|
1164
|
+
if (this._bun && !ssecHeaders && !Object.keys(opts).length) {
|
|
1165
|
+
const f = this._bun.file(key);
|
|
1166
|
+
if (wholeFile) {
|
|
1167
|
+
const buf = await f.arrayBuffer();
|
|
1168
|
+
const stat = await f.stat();
|
|
1169
|
+
return new Response(buf, {
|
|
1170
|
+
status: 200,
|
|
1171
|
+
headers: { 'content-length': String(stat.size), etag: stat.etag, 'content-type': stat.type },
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
const sliced = rangeTo === undefined ? f.slice(rangeFrom) : f.slice(rangeFrom, rangeTo);
|
|
1175
|
+
const buf = await sliced.arrayBuffer();
|
|
1176
|
+
const stat = await f.stat();
|
|
1177
|
+
const endByte = rangeTo === undefined ? stat.size - 1 : rangeTo - 1;
|
|
1178
|
+
return new Response(buf, {
|
|
1179
|
+
status: 206,
|
|
1180
|
+
headers: {
|
|
1181
|
+
'content-range': `bytes ${rangeFrom}-${endByte}/${stat.size}`,
|
|
1182
|
+
'content-length': String(buf.byteLength),
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1042
1186
|
let rangeHdr = {};
|
|
1043
1187
|
if (!wholeFile) {
|
|
1044
1188
|
rangeHdr =
|
|
@@ -1059,6 +1203,9 @@ class S3mini {
|
|
|
1059
1203
|
*/
|
|
1060
1204
|
async getContentLength(key, ssecHeaders) {
|
|
1061
1205
|
try {
|
|
1206
|
+
if (this._bun && !ssecHeaders) {
|
|
1207
|
+
return (await this._bun.file(key).stat()).size;
|
|
1208
|
+
}
|
|
1062
1209
|
const res = await this._signedRequest('HEAD', key, {
|
|
1063
1210
|
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
1064
1211
|
});
|
|
@@ -1080,6 +1227,9 @@ class S3mini {
|
|
|
1080
1227
|
* @returns A promise that resolves to true if the object exists, false if not found, or null if ETag mismatch.
|
|
1081
1228
|
*/
|
|
1082
1229
|
async objectExists(key, opts = {}) {
|
|
1230
|
+
if (this._bun && !Object.keys(opts).length) {
|
|
1231
|
+
return this._bun.file(key).exists();
|
|
1232
|
+
}
|
|
1083
1233
|
const res = await this._signedRequest('HEAD', key, {
|
|
1084
1234
|
query: opts,
|
|
1085
1235
|
tolerated: [200, 404, 412, 304],
|
|
@@ -1106,6 +1256,15 @@ class S3mini {
|
|
|
1106
1256
|
* }
|
|
1107
1257
|
*/
|
|
1108
1258
|
async getEtag(key, opts = {}, ssecHeaders) {
|
|
1259
|
+
if (this._bun && !ssecHeaders && !Object.keys(opts).length) {
|
|
1260
|
+
return this._bunRead(key, async (f) => {
|
|
1261
|
+
const { etag } = await f.stat();
|
|
1262
|
+
if (!etag) {
|
|
1263
|
+
throw new Error(`${ERROR_PREFIX}ETag not found in response headers`);
|
|
1264
|
+
}
|
|
1265
|
+
return sanitizeETag(etag);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1109
1268
|
const res = await this._signedRequest('HEAD', key, {
|
|
1110
1269
|
query: opts,
|
|
1111
1270
|
tolerated: [200, 304, 404, 412],
|
|
@@ -1141,6 +1300,12 @@ class S3mini {
|
|
|
1141
1300
|
* await s3.putObject('image.png', buffer, 'image/png');
|
|
1142
1301
|
*/
|
|
1143
1302
|
async putObject(key, data, fileType = DEFAULT_STREAM_CONTENT_TYPE, ssecHeaders, additionalHeaders, contentLength) {
|
|
1303
|
+
if (this._bun && !ssecHeaders && !additionalHeaders) {
|
|
1304
|
+
const f = this._bun.file(key);
|
|
1305
|
+
await f.write(data, { type: fileType });
|
|
1306
|
+
const { etag } = await f.stat();
|
|
1307
|
+
return new Response(null, { status: 200, headers: etag ? { etag } : {} });
|
|
1308
|
+
}
|
|
1144
1309
|
const size = contentLength ?? getByteSize(data);
|
|
1145
1310
|
return this._signedRequest('PUT', key, {
|
|
1146
1311
|
body: data,
|
|
@@ -1173,6 +1338,14 @@ class S3mini {
|
|
|
1173
1338
|
* await s3.putAnyObject('image.png', buffer, 'image/png');
|
|
1174
1339
|
*/
|
|
1175
1340
|
async putAnyObject(key, data, fileType = DEFAULT_STREAM_CONTENT_TYPE, ssecHeaders, additionalHeaders, contentLength) {
|
|
1341
|
+
// Bun handles multipart automatically for large files
|
|
1342
|
+
if (this._bun && !ssecHeaders && !additionalHeaders) {
|
|
1343
|
+
this._checkKey(key);
|
|
1344
|
+
const f = this._bun.file(key);
|
|
1345
|
+
await f.write(data, { type: fileType });
|
|
1346
|
+
const { etag } = await f.stat();
|
|
1347
|
+
return this._createSuccessResponse(etag || '');
|
|
1348
|
+
}
|
|
1176
1349
|
const size = contentLength ?? getByteSize(data);
|
|
1177
1350
|
// Single PUT for small files
|
|
1178
1351
|
if (!Number.isNaN(size) && size <= this.minPartSize) {
|
|
@@ -1635,72 +1808,74 @@ class S3mini {
|
|
|
1635
1808
|
* @returns A promise that resolves to true if the object was deleted successfully, false otherwise.
|
|
1636
1809
|
*/
|
|
1637
1810
|
async deleteObject(key) {
|
|
1811
|
+
if (this._bun) {
|
|
1812
|
+
await this._bun.file(key).delete();
|
|
1813
|
+
return true;
|
|
1814
|
+
}
|
|
1638
1815
|
const res = await this._signedRequest('DELETE', key, { tolerated: [200, 204] });
|
|
1639
1816
|
return res.status === 200 || res.status === 204;
|
|
1640
1817
|
}
|
|
1641
1818
|
async _deleteObjectsProcess(keys) {
|
|
1819
|
+
const out = await this._sendDeleteRequest(keys);
|
|
1820
|
+
const resultMap = new Map(keys.map(k => [k, false]));
|
|
1821
|
+
this._markDeletedKeys(out, resultMap);
|
|
1822
|
+
this._logDeleteErrors(out, resultMap);
|
|
1823
|
+
return keys.map(key => resultMap.get(key) || false);
|
|
1824
|
+
}
|
|
1825
|
+
async _sendDeleteRequest(keys) {
|
|
1642
1826
|
const objectsXml = keys.map(key => `<Object><Key>${escapeXml(key)}</Key></Object>`).join('');
|
|
1643
1827
|
const xmlBody = '<Delete>' + objectsXml + '</Delete>';
|
|
1644
|
-
const query = { delete: '' };
|
|
1645
1828
|
const sha256base64 = base64FromBuffer(await sha256(xmlBody));
|
|
1646
|
-
const headers = {
|
|
1647
|
-
[HEADER_CONTENT_TYPE]: XML_CONTENT_TYPE,
|
|
1648
|
-
[HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
|
|
1649
|
-
[HEADER_AMZ_CHECKSUM_SHA256]: sha256base64,
|
|
1650
|
-
};
|
|
1651
1829
|
const res = await this._signedRequest('POST', '', {
|
|
1652
|
-
query,
|
|
1830
|
+
query: { delete: '' },
|
|
1653
1831
|
body: xmlBody,
|
|
1654
|
-
headers
|
|
1832
|
+
headers: {
|
|
1833
|
+
[HEADER_CONTENT_TYPE]: XML_CONTENT_TYPE,
|
|
1834
|
+
[HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
|
|
1835
|
+
[HEADER_AMZ_CHECKSUM_SHA256]: sha256base64,
|
|
1836
|
+
},
|
|
1655
1837
|
withQuery: true,
|
|
1656
1838
|
});
|
|
1657
1839
|
const parsed = parseXml(await res.text());
|
|
1658
1840
|
if (!parsed || typeof parsed !== 'object') {
|
|
1659
1841
|
throw new Error(`${ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
|
|
1660
1842
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
resultMap.set(key, false);
|
|
1665
|
-
}
|
|
1843
|
+
return (parsed.DeleteResult || parsed.deleteResult || parsed);
|
|
1844
|
+
}
|
|
1845
|
+
_markDeletedKeys(out, resultMap) {
|
|
1666
1846
|
const deleted = out.deleted || out.Deleted;
|
|
1667
|
-
if (deleted) {
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1847
|
+
if (!deleted) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const items = Array.isArray(deleted) ? deleted : [deleted];
|
|
1851
|
+
for (const item of items) {
|
|
1852
|
+
if (item && typeof item === 'object') {
|
|
1853
|
+
const key = item.key || item.Key;
|
|
1854
|
+
if (key && typeof key === 'string') {
|
|
1855
|
+
resultMap.set(key, true);
|
|
1677
1856
|
}
|
|
1678
1857
|
}
|
|
1679
1858
|
}
|
|
1680
|
-
|
|
1859
|
+
}
|
|
1860
|
+
_logDeleteErrors(out, resultMap) {
|
|
1681
1861
|
const errors = out.error || out.Error;
|
|
1682
|
-
if (errors) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
message: message || 'Unknown error',
|
|
1697
|
-
});
|
|
1698
|
-
}
|
|
1862
|
+
if (!errors) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const items = Array.isArray(errors) ? errors : [errors];
|
|
1866
|
+
for (const item of items) {
|
|
1867
|
+
if (item && typeof item === 'object') {
|
|
1868
|
+
const obj = item;
|
|
1869
|
+
const key = obj.key || obj.Key;
|
|
1870
|
+
if (key && typeof key === 'string') {
|
|
1871
|
+
resultMap.set(key, false);
|
|
1872
|
+
this._log('warn', `Failed to delete object: ${key}`, {
|
|
1873
|
+
code: obj.code || obj.Code || 'Unknown',
|
|
1874
|
+
message: obj.message || obj.Message || 'Unknown error',
|
|
1875
|
+
});
|
|
1699
1876
|
}
|
|
1700
1877
|
}
|
|
1701
1878
|
}
|
|
1702
|
-
// Return boolean array in the same order as input keys
|
|
1703
|
-
return keys.map(key => resultMap.get(key) || false);
|
|
1704
1879
|
}
|
|
1705
1880
|
/**
|
|
1706
1881
|
* Deletes multiple objects from the bucket.
|
|
@@ -1815,6 +1990,9 @@ class S3mini {
|
|
|
1815
1990
|
if (!Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > 604800) {
|
|
1816
1991
|
throw new TypeError(`${ERROR_PREFIX}expiresIn must be between 1 and 604800 seconds`);
|
|
1817
1992
|
}
|
|
1993
|
+
if (this._bun && !Object.keys(queryParams).length && !Object.keys(headers).length) {
|
|
1994
|
+
return this._bun.presign(key, { method, expiresIn: Math.floor(expiresIn) });
|
|
1995
|
+
}
|
|
1818
1996
|
return this._presign(method, uriResourceEscape(key), Math.floor(expiresIn), queryParams, headers);
|
|
1819
1997
|
}
|
|
1820
1998
|
async _presign(method, keyPath, expiresIn, queryParams, headers) {
|