@dimah-s3/server 0.8.6 → 1.0.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/dist/adapters/express.js +13 -1
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.js +13 -1
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/node.js +13 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/svelte-kit.d.ts +2 -0
- package/dist/adapters/svelte-kit.d.ts.map +1 -1
- package/dist/adapters/svelte-kit.js.map +1 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/api/assert-feature-enabled.d.ts.map +1 -1
- package/dist/api/bind-endpoints.d.ts.map +1 -1
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +218 -147
- package/dist/api/index.js.map +1 -1
- package/dist/{router.d.ts → api/router.d.ts} +3 -2
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/routes/confirm.d.ts.map +1 -1
- package/dist/api/routes/delete.d.ts.map +1 -1
- package/dist/api/routes/download.d.ts.map +1 -1
- package/dist/api/routes/index.d.ts +1 -7
- package/dist/api/routes/index.d.ts.map +1 -1
- package/dist/api/routes/multipart/abort.d.ts +1 -5
- package/dist/api/routes/multipart/abort.d.ts.map +1 -1
- package/dist/api/routes/multipart/complete.d.ts.map +1 -1
- package/dist/api/routes/multipart/init.d.ts +0 -2
- package/dist/api/routes/multipart/init.d.ts.map +1 -1
- package/dist/api/routes/multipart/list-parts.d.ts.map +1 -1
- package/dist/api/routes/multipart/part.d.ts.map +1 -1
- package/dist/api/routes/upload.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/helpers/expires.d.ts +6 -0
- package/dist/helpers/expires.d.ts.map +1 -0
- package/dist/helpers/features.d.ts +34 -0
- package/dist/helpers/features.d.ts.map +1 -0
- package/dist/helpers/head-object.d.ts +11 -0
- package/dist/helpers/head-object.d.ts.map +1 -0
- package/dist/{internal-helpers.d.ts → helpers/hooks.d.ts} +1 -4
- package/dist/helpers/hooks.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +12 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/is-aws-not-found.d.ts +3 -1
- package/dist/helpers/is-aws-not-found.d.ts.map +1 -1
- package/dist/helpers/list-parts.d.ts +8 -0
- package/dist/helpers/list-parts.d.ts.map +1 -0
- package/dist/helpers/request.d.ts +3 -0
- package/dist/helpers/request.d.ts.map +1 -0
- package/dist/helpers/resolve-request-acl.d.ts +10 -0
- package/dist/helpers/resolve-request-acl.d.ts.map +1 -0
- package/dist/helpers/resolve-target.d.ts +2 -33
- package/dist/helpers/resolve-target.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -111
- package/dist/index.js.map +1 -1
- package/dist/plugin/apply-plugins.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +5 -5
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugins.js +184 -104
- package/dist/plugins.js.map +1 -1
- package/dist/types/config.d.ts +27 -9
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hook-contexts.d.ts +8 -8
- package/dist/types/hook-contexts.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/internal-helpers.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
package/dist/plugins.js
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition } from '@dimah-s3/core';
|
|
2
|
-
import {
|
|
1
|
+
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, S3_MAX_EXPIRES_IN, buildContentDisposition, S3_MAX_POST_OBJECT_BYTES, S3_DEFAULT_EXPIRES_IN } from '@dimah-s3/core';
|
|
2
|
+
import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand, ListPartsCommand, CompleteMultipartUploadCommand, HeadObjectCommand, AbortMultipartUploadCommand } from '@aws-sdk/client-s3';
|
|
3
3
|
import { createMiddleware, createEndpoint } from 'better-call';
|
|
4
4
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
5
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
6
6
|
|
|
7
7
|
// src/plugin/apply-plugins.ts
|
|
8
|
-
|
|
9
|
-
// src/helpers/is-aws-not-found.ts
|
|
10
|
-
function isAwsNotFound(err) {
|
|
11
|
-
const e = err;
|
|
12
|
-
return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
|
|
13
|
-
}
|
|
14
|
-
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
15
|
-
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
16
|
-
const controller = new AbortController();
|
|
17
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
18
|
-
try {
|
|
19
|
-
const result = await s3.send(
|
|
20
|
-
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
21
|
-
{ abortSignal: controller.signal }
|
|
22
|
-
);
|
|
23
|
-
const isPublic = result.Grants?.some(
|
|
24
|
-
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
25
|
-
);
|
|
26
|
-
return isPublic ? "public-read" : "private";
|
|
27
|
-
} catch {
|
|
28
|
-
return void 0;
|
|
29
|
-
} finally {
|
|
30
|
-
clearTimeout(timeout);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
8
|
var errors = {
|
|
34
9
|
notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
|
|
35
10
|
unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
|
|
@@ -64,13 +39,69 @@ var errors = {
|
|
|
64
39
|
"BAD_REQUEST",
|
|
65
40
|
S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
|
|
66
41
|
),
|
|
42
|
+
multipartPartMissing: (partNumber) => DimahS3Error.from("BAD_REQUEST", {
|
|
43
|
+
code: S3_ERROR_CODES.MULTIPART_PART_MISSING.code,
|
|
44
|
+
message: S3_ERROR_CODES.MULTIPART_PART_MISSING.message.replaceAll(
|
|
45
|
+
"{partNumber}",
|
|
46
|
+
String(partNumber)
|
|
47
|
+
),
|
|
48
|
+
params: { partNumber }
|
|
49
|
+
}),
|
|
50
|
+
payloadTooLarge: (message = S3_ERROR_CODES.PAYLOAD_TOO_LARGE.message) => DimahS3Error.from("PAYLOAD_TOO_LARGE", {
|
|
51
|
+
...S3_ERROR_CODES.PAYLOAD_TOO_LARGE,
|
|
52
|
+
message
|
|
53
|
+
}),
|
|
67
54
|
validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
|
|
68
55
|
code: S3_ERROR_CODES.VALIDATION_ERROR.code,
|
|
69
56
|
message
|
|
70
57
|
})
|
|
71
58
|
};
|
|
72
59
|
|
|
73
|
-
// src/
|
|
60
|
+
// src/helpers/is-aws-not-found.ts
|
|
61
|
+
function isAwsNotFound(err) {
|
|
62
|
+
const e = err;
|
|
63
|
+
return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.name === "NoSuchUpload" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.Code === "NoSuchUpload" || e?.$metadata?.httpStatusCode === 404;
|
|
64
|
+
}
|
|
65
|
+
async function sendOrObjectNotFound(send) {
|
|
66
|
+
try {
|
|
67
|
+
return await send();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (isAwsNotFound(err)) throw errors.objectNotFound();
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
74
|
+
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
77
|
+
try {
|
|
78
|
+
const result = await s3.send(
|
|
79
|
+
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
80
|
+
{ abortSignal: controller.signal }
|
|
81
|
+
);
|
|
82
|
+
const isPublic = result.Grants?.some(
|
|
83
|
+
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
84
|
+
);
|
|
85
|
+
return isPublic ? "public-read" : "private";
|
|
86
|
+
} catch {
|
|
87
|
+
return void 0;
|
|
88
|
+
} finally {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/helpers/resolve-request-acl.ts
|
|
94
|
+
function resolveRequestAcl(policy, requested) {
|
|
95
|
+
if (policy?.acl) return policy.acl;
|
|
96
|
+
if (policy?.allowClientAcl && requested) return requested;
|
|
97
|
+
return "private";
|
|
98
|
+
}
|
|
99
|
+
function normalizeExpiresIn(value, maxExpiresIn = S3_MAX_EXPIRES_IN) {
|
|
100
|
+
const n = Number(value);
|
|
101
|
+
const expires = Number.isFinite(n) && n > 0 ? Math.floor(n) : S3_DEFAULT_EXPIRES_IN;
|
|
102
|
+
const max = Number.isFinite(maxExpiresIn) && maxExpiresIn > 0 ? Math.floor(maxExpiresIn) : S3_MAX_EXPIRES_IN;
|
|
103
|
+
return Math.min(expires, max);
|
|
104
|
+
}
|
|
74
105
|
async function runHook(hook, context) {
|
|
75
106
|
if (!hook) return;
|
|
76
107
|
try {
|
|
@@ -96,10 +127,8 @@ async function runLifecycleHook(hook, context) {
|
|
|
96
127
|
throw errors.internalError();
|
|
97
128
|
}
|
|
98
129
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
|
|
102
|
-
}
|
|
130
|
+
|
|
131
|
+
// src/helpers/request.ts
|
|
103
132
|
function requestFromHeaders(headers) {
|
|
104
133
|
return new Request("http://dimah-s3.local", { headers });
|
|
105
134
|
}
|
|
@@ -155,11 +184,56 @@ async function resolveRequestTarget(config, policy, input) {
|
|
|
155
184
|
});
|
|
156
185
|
return { key, bucket };
|
|
157
186
|
}
|
|
187
|
+
async function headObjectOrNotFound(client, bucket, key) {
|
|
188
|
+
return sendOrObjectNotFound(
|
|
189
|
+
() => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
async function headObjectAfterMultipartComplete(client, bucket, key) {
|
|
193
|
+
const send = () => sendOrObjectNotFound(
|
|
194
|
+
() => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
|
|
195
|
+
);
|
|
196
|
+
let head = await send();
|
|
197
|
+
for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
|
|
198
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
199
|
+
head = await send();
|
|
200
|
+
}
|
|
201
|
+
return head;
|
|
202
|
+
}
|
|
203
|
+
function requireContentLength(head) {
|
|
204
|
+
if (typeof head.ContentLength !== "number") {
|
|
205
|
+
throw errors.internalError();
|
|
206
|
+
}
|
|
207
|
+
return head.ContentLength;
|
|
208
|
+
}
|
|
209
|
+
async function listAllParts(client, input) {
|
|
210
|
+
const parts = [];
|
|
211
|
+
let partNumberMarker;
|
|
212
|
+
let isTruncated = true;
|
|
213
|
+
while (isTruncated) {
|
|
214
|
+
const response = await sendOrObjectNotFound(
|
|
215
|
+
() => client.send(
|
|
216
|
+
new ListPartsCommand({
|
|
217
|
+
Bucket: input.bucket,
|
|
218
|
+
Key: input.key,
|
|
219
|
+
UploadId: input.uploadId,
|
|
220
|
+
...partNumberMarker ? { PartNumberMarker: partNumberMarker } : {}
|
|
221
|
+
})
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
parts.push(...response.Parts ?? []);
|
|
225
|
+
isTruncated = response.IsTruncated === true;
|
|
226
|
+
partNumberMarker = isTruncated ? response.NextPartNumberMarker : void 0;
|
|
227
|
+
}
|
|
228
|
+
return parts;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/helpers/features.ts
|
|
158
232
|
function normalizeFeature(value) {
|
|
159
233
|
if (value === void 0) return void 0;
|
|
160
234
|
if (value === false) return { enabled: false };
|
|
161
235
|
if (value === true) return { enabled: true };
|
|
162
|
-
return { ...value, enabled:
|
|
236
|
+
return { ...value, enabled: true };
|
|
163
237
|
}
|
|
164
238
|
function isFeatureEnabled(config, feature) {
|
|
165
239
|
return config[feature]?.enabled === true;
|
|
@@ -178,7 +252,10 @@ function applyMultipartDefault(config, multipartInput) {
|
|
|
178
252
|
multipart: {
|
|
179
253
|
...next.multipart,
|
|
180
254
|
prefix: next.multipart.prefix ?? next.upload.prefix,
|
|
181
|
-
resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey
|
|
255
|
+
resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey,
|
|
256
|
+
acl: next.multipart.acl ?? next.upload.acl,
|
|
257
|
+
allowClientAcl: next.multipart.allowClientAcl ?? next.upload.allowClientAcl,
|
|
258
|
+
requireFileSize: next.multipart.requireFileSize ?? next.upload.requireFileSize
|
|
182
259
|
}
|
|
183
260
|
};
|
|
184
261
|
}
|
|
@@ -192,6 +269,13 @@ function normalizeFeatures(config) {
|
|
|
192
269
|
multipart: normalizeFeature(config.multipart)
|
|
193
270
|
};
|
|
194
271
|
}
|
|
272
|
+
function assertExclusiveBucketFlags(config) {
|
|
273
|
+
if (config.allowClientBucket && config.buckets?.length) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"dimahS3: set either allowClientBucket or buckets, not both. allowClientBucket honors any client-sent bucket; buckets is an allowlist."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
195
279
|
|
|
196
280
|
// src/api/assert-feature-enabled.ts
|
|
197
281
|
function assertFeatureEnabled(config, feature) {
|
|
@@ -255,9 +339,7 @@ async function handleConfirm(config, input, request) {
|
|
|
255
339
|
key,
|
|
256
340
|
bucket
|
|
257
341
|
});
|
|
258
|
-
const head = await config.client
|
|
259
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
260
|
-
);
|
|
342
|
+
const head = await headObjectOrNotFound(config.client, bucket, key);
|
|
261
343
|
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
|
|
262
344
|
const fileName = parseFileName(head.ContentDisposition);
|
|
263
345
|
const context = {
|
|
@@ -265,7 +347,7 @@ async function handleConfirm(config, input, request) {
|
|
|
265
347
|
key,
|
|
266
348
|
bucket,
|
|
267
349
|
contentType: head.ContentType,
|
|
268
|
-
contentLength: head
|
|
350
|
+
contentLength: requireContentLength(head),
|
|
269
351
|
eTag: head.ETag?.replace(/"/g, ""),
|
|
270
352
|
metadata: head.Metadata,
|
|
271
353
|
acl,
|
|
@@ -306,16 +388,7 @@ async function handleDelete(config, input, request) {
|
|
|
306
388
|
key,
|
|
307
389
|
bucket
|
|
308
390
|
});
|
|
309
|
-
|
|
310
|
-
await config.client.send(
|
|
311
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
312
|
-
);
|
|
313
|
-
} catch (err) {
|
|
314
|
-
if (isAwsNotFound(err)) {
|
|
315
|
-
throw errors.objectNotFound();
|
|
316
|
-
}
|
|
317
|
-
throw err;
|
|
318
|
-
}
|
|
391
|
+
await headObjectOrNotFound(config.client, bucket, key);
|
|
319
392
|
await config.client.send(
|
|
320
393
|
new DeleteObjectCommand({ Bucket: bucket, Key: key })
|
|
321
394
|
);
|
|
@@ -337,7 +410,7 @@ async function handleDownload(config, input, request) {
|
|
|
337
410
|
bucket: input.bucket,
|
|
338
411
|
fileName: input.fileName
|
|
339
412
|
});
|
|
340
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
413
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
341
414
|
const fileName = input.fileName;
|
|
342
415
|
await runHook(config.download?.guard, {
|
|
343
416
|
request,
|
|
@@ -345,16 +418,7 @@ async function handleDownload(config, input, request) {
|
|
|
345
418
|
bucket,
|
|
346
419
|
fileName
|
|
347
420
|
});
|
|
348
|
-
|
|
349
|
-
await config.client.send(
|
|
350
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
351
|
-
);
|
|
352
|
-
} catch (err) {
|
|
353
|
-
if (isAwsNotFound(err)) {
|
|
354
|
-
throw errors.objectNotFound();
|
|
355
|
-
}
|
|
356
|
-
throw err;
|
|
357
|
-
}
|
|
421
|
+
await headObjectOrNotFound(config.client, bucket, key);
|
|
358
422
|
const url = await getSignedUrl(
|
|
359
423
|
config.client,
|
|
360
424
|
new GetObjectCommand({
|
|
@@ -395,12 +459,14 @@ async function handleAbort(config, input, request) {
|
|
|
395
459
|
bucket,
|
|
396
460
|
uploadId
|
|
397
461
|
});
|
|
398
|
-
await
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
462
|
+
await sendOrObjectNotFound(
|
|
463
|
+
() => config.client.send(
|
|
464
|
+
new AbortMultipartUploadCommand({
|
|
465
|
+
Bucket: bucket,
|
|
466
|
+
Key: key,
|
|
467
|
+
UploadId: uploadId
|
|
468
|
+
})
|
|
469
|
+
)
|
|
404
470
|
);
|
|
405
471
|
await runLifecycleHook(config.multipart?.onAbort, {
|
|
406
472
|
request,
|
|
@@ -434,32 +500,34 @@ async function handleComplete(config, input, request) {
|
|
|
434
500
|
uploadId,
|
|
435
501
|
parts: partRefs
|
|
436
502
|
});
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
503
|
+
const listedParts = await listAllParts(config.client, {
|
|
504
|
+
bucket,
|
|
505
|
+
key,
|
|
506
|
+
uploadId
|
|
507
|
+
});
|
|
441
508
|
const completeParts = parts.map((partNumber) => {
|
|
442
509
|
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
443
|
-
|
|
510
|
+
if (!found?.ETag) {
|
|
511
|
+
throw errors.multipartPartMissing(partNumber);
|
|
512
|
+
}
|
|
513
|
+
return { PartNumber: partNumber, ETag: found.ETag };
|
|
444
514
|
});
|
|
445
|
-
const completeResult = await
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
515
|
+
const completeResult = await sendOrObjectNotFound(
|
|
516
|
+
() => config.client.send(
|
|
517
|
+
new CompleteMultipartUploadCommand({
|
|
518
|
+
Bucket: bucket,
|
|
519
|
+
Key: key,
|
|
520
|
+
UploadId: uploadId,
|
|
521
|
+
MultipartUpload: { Parts: completeParts }
|
|
522
|
+
})
|
|
523
|
+
)
|
|
452
524
|
);
|
|
453
|
-
|
|
454
|
-
|
|
525
|
+
const head = await headObjectAfterMultipartComplete(
|
|
526
|
+
config.client,
|
|
527
|
+
bucket,
|
|
528
|
+
key
|
|
455
529
|
);
|
|
456
|
-
|
|
457
|
-
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
458
|
-
head = await config.client.send(
|
|
459
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
const contentLength = head.ContentLength ?? 0;
|
|
530
|
+
const contentLength = requireContentLength(head);
|
|
463
531
|
const contentType = head.ContentType;
|
|
464
532
|
const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
|
|
465
533
|
const metadata = head.Metadata ?? {};
|
|
@@ -511,7 +579,7 @@ async function handleMultipartInit(config, input, request) {
|
|
|
511
579
|
fileName: input.fileName,
|
|
512
580
|
contentType: input.contentType
|
|
513
581
|
});
|
|
514
|
-
const acl = input.acl
|
|
582
|
+
const acl = resolveRequestAcl(config.multipart, input.acl);
|
|
515
583
|
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
|
|
516
584
|
if (config.multipart?.requireFileSize && fileSize === void 0) {
|
|
517
585
|
throw errors.fileSizeRequiredMultipart();
|
|
@@ -520,7 +588,11 @@ async function handleMultipartInit(config, input, request) {
|
|
|
520
588
|
request,
|
|
521
589
|
key,
|
|
522
590
|
bucket,
|
|
523
|
-
fileSize
|
|
591
|
+
fileSize,
|
|
592
|
+
contentType: input.contentType,
|
|
593
|
+
metadata: input.metadata,
|
|
594
|
+
acl,
|
|
595
|
+
fileName: input.fileName
|
|
524
596
|
});
|
|
525
597
|
const { UploadId } = await config.client.send(
|
|
526
598
|
new CreateMultipartUploadCommand({
|
|
@@ -532,6 +604,9 @@ async function handleMultipartInit(config, input, request) {
|
|
|
532
604
|
ACL: acl
|
|
533
605
|
})
|
|
534
606
|
);
|
|
607
|
+
if (!UploadId) {
|
|
608
|
+
throw errors.internalError();
|
|
609
|
+
}
|
|
535
610
|
await runLifecycleHook(config.multipart?.onInit, {
|
|
536
611
|
request,
|
|
537
612
|
key,
|
|
@@ -571,14 +646,8 @@ async function handleListParts(config, input, request) {
|
|
|
571
646
|
bucket,
|
|
572
647
|
uploadId
|
|
573
648
|
});
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
Bucket: bucket,
|
|
577
|
-
Key: key,
|
|
578
|
-
UploadId: uploadId
|
|
579
|
-
})
|
|
580
|
-
);
|
|
581
|
-
const parts = (response.Parts ?? []).map((p) => ({
|
|
649
|
+
const listed = await listAllParts(config.client, { bucket, key, uploadId });
|
|
650
|
+
const parts = listed.map((p) => ({
|
|
582
651
|
partNumber: p.PartNumber ?? 0,
|
|
583
652
|
size: p.Size ?? 0,
|
|
584
653
|
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
@@ -608,7 +677,7 @@ async function handleSignPart(config, input, request) {
|
|
|
608
677
|
});
|
|
609
678
|
const uploadId = input.uploadId;
|
|
610
679
|
const partNumber = input.partNumber;
|
|
611
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
680
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
612
681
|
const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
|
|
613
682
|
await runHook(config.multipart?.partGuard, {
|
|
614
683
|
request,
|
|
@@ -649,6 +718,14 @@ var multipartPart = createS3Endpoint(
|
|
|
649
718
|
return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
|
|
650
719
|
}
|
|
651
720
|
);
|
|
721
|
+
function putMetadataHeaders(metadata) {
|
|
722
|
+
if (!metadata) return {};
|
|
723
|
+
const headers = {};
|
|
724
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
725
|
+
headers[`x-amz-meta-${k}`] = v;
|
|
726
|
+
}
|
|
727
|
+
return headers;
|
|
728
|
+
}
|
|
652
729
|
async function handleUpload(config, input, request) {
|
|
653
730
|
const { key, bucket } = await resolveRequestTarget(config, config.upload, {
|
|
654
731
|
request,
|
|
@@ -657,10 +734,13 @@ async function handleUpload(config, input, request) {
|
|
|
657
734
|
fileName: input.fileName,
|
|
658
735
|
contentType: input.contentType
|
|
659
736
|
});
|
|
660
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
661
|
-
const acl = input.acl
|
|
737
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
738
|
+
const acl = resolveRequestAcl(config.upload, input.acl);
|
|
662
739
|
const contentType = input.contentType ?? "application/octet-stream";
|
|
663
740
|
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
|
|
741
|
+
if (config.upload?.requireFileSize && fileSize === null) {
|
|
742
|
+
throw errors.fileSizeRequiredUpload();
|
|
743
|
+
}
|
|
664
744
|
await runHook(config.upload?.guard, {
|
|
665
745
|
request,
|
|
666
746
|
key,
|
|
@@ -672,12 +752,10 @@ async function handleUpload(config, input, request) {
|
|
|
672
752
|
fileName: input.fileName
|
|
673
753
|
});
|
|
674
754
|
const method = config.upload?.method ?? "POST";
|
|
675
|
-
if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
|
|
676
|
-
throw errors.fileSizeRequiredUpload();
|
|
677
|
-
}
|
|
678
755
|
if (method === "PUT") {
|
|
679
756
|
const putHeaders = {
|
|
680
|
-
"Content-Type": contentType
|
|
757
|
+
"Content-Type": contentType,
|
|
758
|
+
...putMetadataHeaders(input.metadata)
|
|
681
759
|
};
|
|
682
760
|
if (input.fileName) {
|
|
683
761
|
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
@@ -691,6 +769,7 @@ async function handleUpload(config, input, request) {
|
|
|
691
769
|
Key: key,
|
|
692
770
|
ContentType: contentType,
|
|
693
771
|
ACL: acl,
|
|
772
|
+
Metadata: input.metadata,
|
|
694
773
|
...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
|
|
695
774
|
...fileSize !== null ? { ContentLength: fileSize } : {}
|
|
696
775
|
}),
|
|
@@ -736,7 +815,7 @@ async function handleUpload(config, input, request) {
|
|
|
736
815
|
{
|
|
737
816
|
Bucket: bucket,
|
|
738
817
|
Key: key,
|
|
739
|
-
Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin,
|
|
818
|
+
Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, S3_MAX_POST_OBJECT_BYTES]],
|
|
740
819
|
Fields: fields,
|
|
741
820
|
Expires: expiresIn
|
|
742
821
|
}
|
|
@@ -938,6 +1017,7 @@ function applyPlugins(config) {
|
|
|
938
1017
|
}
|
|
939
1018
|
}
|
|
940
1019
|
const { plugins: _omit, ...rest } = config;
|
|
1020
|
+
assertExclusiveBucketFlags(rest);
|
|
941
1021
|
for (const plugin of plugins) {
|
|
942
1022
|
plugin.init?.({ config: rest, getPlugin });
|
|
943
1023
|
}
|