@dimah-s3/server 1.0.2 → 1.1.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 +7 -3
- package/dist/adapters/express.js +16 -9
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.js +16 -9
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/hono.d.ts +1 -1
- package/dist/adapters/hono.js.map +1 -1
- package/dist/adapters/node.d.ts +1 -1
- package/dist/adapters/node.js +16 -9
- package/dist/adapters/node.js.map +1 -1
- package/dist/api/index.js +742 -388
- package/dist/api/index.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/routes/catalog.d.ts +9 -0
- package/dist/api/routes/catalog.d.ts.map +1 -0
- package/dist/api/routes/confirm.d.ts +3 -4
- package/dist/api/routes/confirm.d.ts.map +1 -1
- package/dist/api/routes/delete-batch.d.ts +14 -0
- package/dist/api/routes/delete-batch.d.ts.map +1 -0
- package/dist/api/routes/delete.d.ts +2 -2
- package/dist/api/routes/delete.d.ts.map +1 -1
- package/dist/api/routes/download.d.ts +8 -8
- package/dist/api/routes/download.d.ts.map +1 -1
- package/dist/api/routes/file.d.ts +20 -0
- package/dist/api/routes/file.d.ts.map +1 -0
- package/dist/api/routes/index.d.ts +76 -50
- package/dist/api/routes/index.d.ts.map +1 -1
- package/dist/api/routes/multipart/abort.d.ts +2 -2
- package/dist/api/routes/multipart/abort.d.ts.map +1 -1
- package/dist/api/routes/multipart/complete.d.ts +2 -2
- package/dist/api/routes/multipart/complete.d.ts.map +1 -1
- package/dist/api/routes/multipart/init.d.ts +10 -12
- package/dist/api/routes/multipart/init.d.ts.map +1 -1
- package/dist/api/routes/multipart/list-parts.d.ts +2 -2
- package/dist/api/routes/multipart/list-parts.d.ts.map +1 -1
- package/dist/api/routes/multipart/part.d.ts +4 -6
- package/dist/api/routes/multipart/part.d.ts.map +1 -1
- package/dist/api/routes/upload.d.ts +10 -14
- package/dist/api/routes/upload.d.ts.map +1 -1
- package/dist/dimah-s3.d.ts +19 -8
- package/dist/dimah-s3.d.ts.map +1 -1
- package/dist/errors.d.ts +5 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/helpers/best-effort.d.ts +6 -0
- package/dist/helpers/best-effort.d.ts.map +1 -0
- package/dist/helpers/constraints.d.ts +16 -0
- package/dist/helpers/constraints.d.ts.map +1 -0
- package/dist/helpers/expires.d.ts +2 -2
- package/dist/helpers/features.d.ts +12 -29
- package/dist/helpers/features.d.ts.map +1 -1
- package/dist/helpers/finalize-confirmed.d.ts +19 -0
- package/dist/helpers/finalize-confirmed.d.ts.map +1 -0
- package/dist/helpers/head-object.d.ts.map +1 -1
- package/dist/helpers/hooks.d.ts +7 -1
- package/dist/helpers/hooks.d.ts.map +1 -1
- package/dist/helpers/index.d.ts +12 -9
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/list-parts.d.ts +5 -0
- package/dist/helpers/list-parts.d.ts.map +1 -1
- package/dist/helpers/logger.d.ts +4 -0
- package/dist/helpers/logger.d.ts.map +1 -0
- package/dist/helpers/previous-key.d.ts +5 -0
- package/dist/helpers/previous-key.d.ts.map +1 -0
- package/dist/helpers/resolve-route.d.ts +36 -0
- package/dist/helpers/resolve-route.d.ts.map +1 -0
- package/dist/helpers/resolve-target.d.ts +25 -14
- package/dist/helpers/resolve-target.d.ts.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1017 -477
- package/dist/index.js.map +1 -1
- package/dist/plugin/apply-plugins.d.ts +9 -4
- package/dist/plugin/apply-plugins.d.ts.map +1 -1
- package/dist/plugin/hook-registry.d.ts +13 -4
- package/dist/plugin/hook-registry.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +1 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +12 -7
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugins.d.ts +5 -3
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +951 -475
- package/dist/plugins.js.map +1 -1
- package/dist/put.d.ts +15 -0
- package/dist/put.d.ts.map +1 -0
- package/dist/route.d.ts +13 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/types/config.d.ts +190 -95
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hook-contexts.d.ts +124 -87
- package/dist/types/hook-contexts.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -11
- package/dist/api/assert-feature-enabled.d.ts +0 -5
- package/dist/api/assert-feature-enabled.d.ts.map +0 -1
- package/dist/helpers/resolve-object-acl.d.ts +0 -10
- package/dist/helpers/resolve-object-acl.d.ts.map +0 -1
- package/dist/helpers/resolve-request-acl.d.ts +0 -10
- package/dist/helpers/resolve-request-acl.d.ts.map +0 -1
package/dist/api/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createMiddleware, createEndpoint } from 'better-call';
|
|
2
|
-
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema,
|
|
3
|
-
import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand,
|
|
2
|
+
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, isDimahS3Error, deleteBatchBodySchema, downloadQuerySchema, fileQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, S3_MAX_EXPIRES_IN, buildContentDisposition, S3_MAX_POST_OBJECT_BYTES, resolveStoredFileName, S3_DEFAULT_EXPIRES_IN, matchesFileTypes, normalizeObjectKey, sanitizeFileName } from '@dimah-s3/core';
|
|
3
|
+
import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, ListPartsCommand, AbortMultipartUploadCommand, CompleteMultipartUploadCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
4
4
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
5
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
6
6
|
|
|
@@ -21,10 +21,18 @@ var errors = {
|
|
|
21
21
|
params: { feature }
|
|
22
22
|
}),
|
|
23
23
|
invalidKey: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.INVALID_KEY),
|
|
24
|
-
|
|
25
|
-
code: S3_ERROR_CODES.
|
|
26
|
-
message: S3_ERROR_CODES.
|
|
27
|
-
params: {
|
|
24
|
+
unknownRoute: (route) => DimahS3Error.from("NOT_FOUND", {
|
|
25
|
+
code: S3_ERROR_CODES.UNKNOWN_ROUTE.code,
|
|
26
|
+
message: S3_ERROR_CODES.UNKNOWN_ROUTE.message,
|
|
27
|
+
params: { route }
|
|
28
|
+
}),
|
|
29
|
+
fileTypeNotAllowed: (fileName, contentType) => DimahS3Error.from("BAD_REQUEST", {
|
|
30
|
+
code: S3_ERROR_CODES.FILE_TYPE_NOT_ALLOWED.code,
|
|
31
|
+
message: S3_ERROR_CODES.FILE_TYPE_NOT_ALLOWED.message,
|
|
32
|
+
params: {
|
|
33
|
+
fileName,
|
|
34
|
+
...contentType ? { contentType } : {}
|
|
35
|
+
}
|
|
28
36
|
}),
|
|
29
37
|
s3NetworkError: (code) => DimahS3Error.from("BAD_GATEWAY", {
|
|
30
38
|
code: S3_ERROR_CODES.S3_NETWORK_ERROR.code,
|
|
@@ -34,11 +42,6 @@ var errors = {
|
|
|
34
42
|
),
|
|
35
43
|
params: { code }
|
|
36
44
|
}),
|
|
37
|
-
fileSizeRequiredUpload: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD),
|
|
38
|
-
fileSizeRequiredMultipart: () => DimahS3Error.from(
|
|
39
|
-
"BAD_REQUEST",
|
|
40
|
-
S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
|
|
41
|
-
),
|
|
42
45
|
multipartPartMissing: (partNumber) => DimahS3Error.from("BAD_REQUEST", {
|
|
43
46
|
code: S3_ERROR_CODES.MULTIPART_PART_MISSING.code,
|
|
44
47
|
message: S3_ERROR_CODES.MULTIPART_PART_MISSING.message.replaceAll(
|
|
@@ -51,11 +54,33 @@ var errors = {
|
|
|
51
54
|
...S3_ERROR_CODES.PAYLOAD_TOO_LARGE,
|
|
52
55
|
message
|
|
53
56
|
}),
|
|
57
|
+
quotaExceeded: (message = S3_ERROR_CODES.QUOTA_EXCEEDED.message) => DimahS3Error.from("PAYLOAD_TOO_LARGE", {
|
|
58
|
+
...S3_ERROR_CODES.QUOTA_EXCEEDED,
|
|
59
|
+
message
|
|
60
|
+
}),
|
|
54
61
|
validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
|
|
55
62
|
code: S3_ERROR_CODES.VALIDATION_ERROR.code,
|
|
56
63
|
message
|
|
57
64
|
})
|
|
58
65
|
};
|
|
66
|
+
async function deleteObjectBestEffort(client, bucket, key) {
|
|
67
|
+
try {
|
|
68
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function abortMultipartBestEffort(client, bucket, key, uploadId) {
|
|
73
|
+
try {
|
|
74
|
+
await client.send(
|
|
75
|
+
new AbortMultipartUploadCommand({
|
|
76
|
+
Bucket: bucket,
|
|
77
|
+
Key: key,
|
|
78
|
+
UploadId: uploadId
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
59
84
|
|
|
60
85
|
// src/helpers/is-aws-not-found.ts
|
|
61
86
|
function isAwsNotFound(err) {
|
|
@@ -70,119 +95,238 @@ async function sendOrObjectNotFound(send) {
|
|
|
70
95
|
throw err;
|
|
71
96
|
}
|
|
72
97
|
}
|
|
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
98
|
function normalizeExpiresIn(value, maxExpiresIn = S3_MAX_EXPIRES_IN) {
|
|
100
99
|
const n = Number(value);
|
|
101
100
|
const expires = Number.isFinite(n) && n > 0 ? Math.floor(n) : S3_DEFAULT_EXPIRES_IN;
|
|
102
101
|
const max = Number.isFinite(maxExpiresIn) && maxExpiresIn > 0 ? Math.floor(maxExpiresIn) : S3_MAX_EXPIRES_IN;
|
|
103
102
|
return Math.min(expires, max);
|
|
104
103
|
}
|
|
104
|
+
|
|
105
|
+
// src/helpers/logger.ts
|
|
106
|
+
var defaultLogger = {
|
|
107
|
+
error: (message, ...args) => {
|
|
108
|
+
console.error("[dimah-s3]", message, ...args);
|
|
109
|
+
},
|
|
110
|
+
warn: (message, ...args) => {
|
|
111
|
+
console.warn("[dimah-s3]", message, ...args);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function resolveLogger(logger) {
|
|
115
|
+
if (logger === false) return {};
|
|
116
|
+
if (!logger) return defaultLogger;
|
|
117
|
+
return logger;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/helpers/hooks.ts
|
|
121
|
+
function mapGuardError(err) {
|
|
122
|
+
if (isAPIError(err)) throw err;
|
|
123
|
+
if (err instanceof Error && err.message.trim()) {
|
|
124
|
+
throw new DimahS3Error("FORBIDDEN", {
|
|
125
|
+
message: err.message,
|
|
126
|
+
cause: err
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
throw errors.forbidden();
|
|
130
|
+
}
|
|
105
131
|
async function runHook(hook, context) {
|
|
106
132
|
if (!hook) return;
|
|
107
133
|
try {
|
|
108
134
|
await hook(context);
|
|
109
135
|
} catch (err) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
mapGuardError(err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function runObjectHook(hook, context) {
|
|
140
|
+
if (!hook) return;
|
|
141
|
+
try {
|
|
142
|
+
return await hook(context);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
mapGuardError(err);
|
|
118
145
|
}
|
|
119
146
|
}
|
|
120
|
-
async function runLifecycleHook(hook, context) {
|
|
147
|
+
async function runLifecycleHook(hook, context, config) {
|
|
121
148
|
if (!hook) return;
|
|
122
149
|
try {
|
|
123
150
|
await hook(context);
|
|
124
151
|
} catch (err) {
|
|
125
152
|
if (isAPIError(err)) throw err;
|
|
126
|
-
|
|
153
|
+
resolveLogger(config?.logger).error?.("lifecycle hook failed", err);
|
|
154
|
+
config?.onError?.(err, { request: context.request });
|
|
127
155
|
throw errors.internalError();
|
|
128
156
|
}
|
|
129
157
|
}
|
|
130
158
|
|
|
131
|
-
// src/helpers/
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
// src/helpers/previous-key.ts
|
|
160
|
+
var DIMAH_PREVIOUS_KEY_META = "dimah-previous-key";
|
|
161
|
+
function stripPreviousKeyMeta(metadata) {
|
|
162
|
+
if (!metadata) return {};
|
|
163
|
+
const { [DIMAH_PREVIOUS_KEY_META]: _omit, ...rest } = metadata;
|
|
164
|
+
return rest;
|
|
165
|
+
}
|
|
166
|
+
function previousKeyFromMetadata(metadata) {
|
|
167
|
+
const value = metadata?.[DIMAH_PREVIOUS_KEY_META]?.trim();
|
|
168
|
+
return value || void 0;
|
|
134
169
|
}
|
|
135
170
|
|
|
136
171
|
// src/helpers/resolve-target.ts
|
|
137
172
|
function assertSafeObjectKey(key) {
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
if (trimmed.includes("\\") || trimmed.includes("\0")) {
|
|
143
|
-
throw errors.invalidKey();
|
|
144
|
-
}
|
|
145
|
-
const parts = trimmed.split("/");
|
|
146
|
-
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
147
|
-
throw errors.invalidKey();
|
|
148
|
-
}
|
|
149
|
-
return parts.join("/");
|
|
173
|
+
const normalized = normalizeObjectKey(key);
|
|
174
|
+
if (!normalized) throw errors.invalidKey();
|
|
175
|
+
return normalized;
|
|
150
176
|
}
|
|
151
177
|
function applyPrefix(prefix, key) {
|
|
152
178
|
const p = assertSafeObjectKey(prefix);
|
|
153
179
|
const k = assertSafeObjectKey(key);
|
|
154
|
-
if (k
|
|
180
|
+
if (k.startsWith(`${p}/`)) return k;
|
|
181
|
+
if (k === p) throw errors.invalidKey();
|
|
155
182
|
return `${p}/${k}`;
|
|
156
183
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
184
|
+
function nestKeyUnderPrefix(keyPrefix, key) {
|
|
185
|
+
const safe = assertSafeObjectKey(key);
|
|
186
|
+
if (keyPrefix === false) return safe;
|
|
187
|
+
return applyPrefix(keyPrefix, safe);
|
|
188
|
+
}
|
|
189
|
+
function assertStoredKey(key, keyPrefix) {
|
|
190
|
+
const safe = assertSafeObjectKey(key);
|
|
191
|
+
if (keyPrefix === false) return safe;
|
|
192
|
+
const root = assertSafeObjectKey(keyPrefix);
|
|
193
|
+
if (safe.startsWith(`${root}/`)) return safe;
|
|
194
|
+
throw errors.invalidKey();
|
|
195
|
+
}
|
|
196
|
+
function objectKeyFileName(fileName) {
|
|
197
|
+
const sanitized = sanitizeFileName(fileName);
|
|
198
|
+
const leaf = sanitized.split("/").filter((part) => part.length > 0).at(-1);
|
|
199
|
+
if (!leaf || leaf === "." || leaf === "..") return "file";
|
|
200
|
+
return leaf;
|
|
201
|
+
}
|
|
202
|
+
function generateObjectKey(folder, fileName) {
|
|
203
|
+
const leaf = `${crypto.randomUUID()}/${objectKeyFileName(fileName)}`;
|
|
204
|
+
if (folder === false) return assertSafeObjectKey(leaf);
|
|
205
|
+
return applyPrefix(folder, leaf);
|
|
206
|
+
}
|
|
207
|
+
function resolveAcl(info, route) {
|
|
208
|
+
return info?.acl ?? route.upload.acl ?? "private";
|
|
209
|
+
}
|
|
210
|
+
async function resolveUploadTarget(route, context) {
|
|
211
|
+
const bucket = route.bucket;
|
|
212
|
+
const objectContext = {
|
|
213
|
+
...context,
|
|
214
|
+
bucket,
|
|
215
|
+
keyPrefix: route.keyPrefix
|
|
216
|
+
};
|
|
217
|
+
const info = await runObjectHook(route.upload.object, objectContext) ?? void 0;
|
|
218
|
+
const key = info?.key ? nestKeyUnderPrefix(route.keyPrefix, info.key) : generateObjectKey(
|
|
219
|
+
info?.folder != null && info.folder !== "" ? nestKeyUnderPrefix(route.keyPrefix, info.folder) : route.keyPrefix === false ? false : route.keyPrefix,
|
|
220
|
+
context.file.name
|
|
221
|
+
);
|
|
222
|
+
const metadata = { ...info?.metadata };
|
|
223
|
+
if (info?.previousKey) {
|
|
224
|
+
metadata[DIMAH_PREVIOUS_KEY_META] = nestKeyUnderPrefix(
|
|
225
|
+
route.keyPrefix,
|
|
226
|
+
info.previousKey
|
|
227
|
+
);
|
|
160
228
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
229
|
+
return {
|
|
230
|
+
key,
|
|
231
|
+
bucket,
|
|
232
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : info?.metadata,
|
|
233
|
+
acl: resolveAcl(info, route)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function resolveStoredTarget(route, key) {
|
|
237
|
+
return {
|
|
238
|
+
key: assertStoredKey(key, route.keyPrefix),
|
|
239
|
+
bucket: route.bucket
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/helpers/features.ts
|
|
244
|
+
function isEnabled(feature) {
|
|
245
|
+
return feature.enabled;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/helpers/request.ts
|
|
249
|
+
function requestFromHeaders(headers) {
|
|
250
|
+
return new Request("http://dimah-s3.local", { headers });
|
|
251
|
+
}
|
|
252
|
+
function assertWithinMaxFileSize(maxFileSize, size) {
|
|
253
|
+
if (typeof maxFileSize === "number" && size > maxFileSize) {
|
|
254
|
+
throw errors.payloadTooLarge();
|
|
164
255
|
}
|
|
165
|
-
return assertSafeObjectKey(context.proposedKey);
|
|
166
256
|
}
|
|
167
|
-
function
|
|
168
|
-
if (!
|
|
169
|
-
|
|
170
|
-
if (config.buckets?.length) {
|
|
171
|
-
if (config.buckets.includes(requested)) return requested;
|
|
172
|
-
throw errors.invalidBucket(requested);
|
|
257
|
+
function assertDeclaredConstraints(constraints, input) {
|
|
258
|
+
if (constraints?.fileTypes?.length && !matchesFileTypes(input.fileName, input.contentType, constraints.fileTypes)) {
|
|
259
|
+
throw errors.fileTypeNotAllowed(input.fileName, input.contentType);
|
|
173
260
|
}
|
|
174
|
-
|
|
261
|
+
assertWithinMaxFileSize(constraints?.maxFileSize, input.fileSize);
|
|
175
262
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
263
|
+
function assertVerifiedConstraints(constraints, input) {
|
|
264
|
+
assertWithinMaxFileSize(constraints?.maxFileSize, input.contentLength);
|
|
265
|
+
const name = input.fileName ?? "";
|
|
266
|
+
if (constraints?.fileTypes?.length && !matchesFileTypes(name, input.contentType, constraints.fileTypes)) {
|
|
267
|
+
throw errors.fileTypeNotAllowed(name || "unknown", input.contentType);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/helpers/resolve-route.ts
|
|
272
|
+
function getResolvedRoute(config, name) {
|
|
273
|
+
const route = config.routes[name];
|
|
274
|
+
if (!route) {
|
|
275
|
+
throw errors.unknownRoute(name);
|
|
276
|
+
}
|
|
277
|
+
return route;
|
|
278
|
+
}
|
|
279
|
+
function featureIsEnabled(route, feature) {
|
|
280
|
+
if (feature === "multipart") return route.upload.multipart.enabled;
|
|
281
|
+
return route[feature].enabled;
|
|
282
|
+
}
|
|
283
|
+
function assertFeatureEnabled(route, feature) {
|
|
284
|
+
if (!featureIsEnabled(route, feature)) {
|
|
285
|
+
throw errors.featureDisabled(feature);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function openRoute(config, name, request, feature) {
|
|
289
|
+
const route = getResolvedRoute(config, name);
|
|
290
|
+
assertFeatureEnabled(route, feature);
|
|
291
|
+
await runHook(route.guard, { request, route: route.name });
|
|
292
|
+
return route;
|
|
293
|
+
}
|
|
294
|
+
function storedObjectContext(request, route, key) {
|
|
295
|
+
return { request, route: route.name, key, bucket: route.bucket };
|
|
296
|
+
}
|
|
297
|
+
async function openUploadTarget(config, input, request, feature) {
|
|
298
|
+
const route = await openRoute(config, input.route, request, feature);
|
|
299
|
+
const fileSize = Math.floor(input.fileSize);
|
|
300
|
+
const fileName = input.fileName;
|
|
301
|
+
assertDeclaredConstraints(route.upload, {
|
|
302
|
+
fileName,
|
|
303
|
+
fileSize,
|
|
304
|
+
contentType: input.contentType
|
|
305
|
+
});
|
|
306
|
+
const target = await resolveUploadTarget(route, {
|
|
307
|
+
request,
|
|
308
|
+
route: route.name,
|
|
309
|
+
file: {
|
|
310
|
+
name: fileName,
|
|
311
|
+
size: fileSize,
|
|
312
|
+
type: input.contentType
|
|
313
|
+
},
|
|
314
|
+
clientMetadata: input.metadata
|
|
184
315
|
});
|
|
185
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
route,
|
|
318
|
+
...target,
|
|
319
|
+
stored: storedObjectContext(request, route, target.key)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async function openStoredTarget(config, input, request, feature) {
|
|
323
|
+
const route = await openRoute(config, input.route, request, feature);
|
|
324
|
+
const target = resolveStoredTarget(route, input.key);
|
|
325
|
+
return {
|
|
326
|
+
route,
|
|
327
|
+
...target,
|
|
328
|
+
stored: storedObjectContext(request, route, target.key)
|
|
329
|
+
};
|
|
186
330
|
}
|
|
187
331
|
async function headObjectOrNotFound(client, bucket, key) {
|
|
188
332
|
return sendOrObjectNotFound(
|
|
@@ -194,7 +338,7 @@ async function headObjectAfterMultipartComplete(client, bucket, key) {
|
|
|
194
338
|
() => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
|
|
195
339
|
);
|
|
196
340
|
let head = await send();
|
|
197
|
-
for (let attempt = 0; attempt < 4 &&
|
|
341
|
+
for (let attempt = 0; attempt < 4 && typeof head.ContentLength !== "number"; attempt++) {
|
|
198
342
|
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
199
343
|
head = await send();
|
|
200
344
|
}
|
|
@@ -227,10 +371,70 @@ async function listAllParts(client, input) {
|
|
|
227
371
|
}
|
|
228
372
|
return parts;
|
|
229
373
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
374
|
+
function listedPartsByteSize(parts, excludePartNumber) {
|
|
375
|
+
let total = 0;
|
|
376
|
+
for (const part of parts) {
|
|
377
|
+
if (excludePartNumber !== void 0 && part.PartNumber === excludePartNumber) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
total += part.Size ?? 0;
|
|
381
|
+
}
|
|
382
|
+
return total;
|
|
383
|
+
}
|
|
384
|
+
async function finalizeConfirmedObject(client, bucket, key, input) {
|
|
385
|
+
const { config, route, stored, head, uploadId, acl } = input;
|
|
386
|
+
const contentLength = requireContentLength(head);
|
|
387
|
+
const fileName = resolveStoredFileName(head.ContentDisposition, key);
|
|
388
|
+
const rawMetadata = head.Metadata ?? {};
|
|
389
|
+
try {
|
|
390
|
+
assertVerifiedConstraints(route.upload, {
|
|
391
|
+
fileName,
|
|
392
|
+
contentType: head.ContentType,
|
|
393
|
+
contentLength
|
|
394
|
+
});
|
|
395
|
+
} catch (err) {
|
|
396
|
+
await deleteObjectBestEffort(client, bucket, key);
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
const metadata = stripPreviousKeyMeta(rawMetadata);
|
|
400
|
+
const context = {
|
|
401
|
+
...stored,
|
|
402
|
+
contentType: head.ContentType,
|
|
403
|
+
contentLength,
|
|
404
|
+
eTag: head.ETag?.replace(/"/g, ""),
|
|
405
|
+
metadata,
|
|
406
|
+
acl,
|
|
407
|
+
fileName,
|
|
408
|
+
versionId: head.VersionId,
|
|
409
|
+
lastModified: head.LastModified?.toISOString(),
|
|
410
|
+
uploadId
|
|
411
|
+
};
|
|
412
|
+
try {
|
|
413
|
+
await runLifecycleHook(route.upload.onConfirmed, context, config);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
await deleteObjectBestEffort(client, bucket, key);
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
const previousKey = previousKeyFromMetadata(rawMetadata);
|
|
419
|
+
if (previousKey && previousKey !== key) {
|
|
420
|
+
try {
|
|
421
|
+
const safe = assertStoredKey(previousKey, route.keyPrefix);
|
|
422
|
+
await deleteObjectBestEffort(client, bucket, safe);
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
key,
|
|
428
|
+
bucket,
|
|
429
|
+
contentType: context.contentType,
|
|
430
|
+
contentLength: context.contentLength,
|
|
431
|
+
eTag: context.eTag,
|
|
432
|
+
metadata: context.metadata ?? {},
|
|
433
|
+
acl,
|
|
434
|
+
fileName,
|
|
435
|
+
versionId: context.versionId,
|
|
436
|
+
lastModified: context.lastModified
|
|
437
|
+
};
|
|
234
438
|
}
|
|
235
439
|
|
|
236
440
|
// src/api/create-s3-middleware.ts
|
|
@@ -277,148 +481,270 @@ var createS3Endpoint = ((pathOrOptions, optionsOrHandler, maybeHandler) => {
|
|
|
277
481
|
optionsOrHandler
|
|
278
482
|
);
|
|
279
483
|
});
|
|
280
|
-
|
|
281
|
-
// src/api/assert-feature-enabled.ts
|
|
282
|
-
function assertFeatureEnabled(config, feature) {
|
|
283
|
-
if (!isFeatureEnabled(config, feature)) {
|
|
284
|
-
throw errors.featureDisabled(feature);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// src/api/routes/confirm.ts
|
|
289
484
|
async function handleConfirm(config, input, request) {
|
|
290
|
-
const { key, bucket } = await
|
|
485
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
486
|
+
config,
|
|
487
|
+
input,
|
|
291
488
|
request,
|
|
292
|
-
|
|
293
|
-
|
|
489
|
+
"upload"
|
|
490
|
+
);
|
|
491
|
+
await runHook(route.upload.confirmGuard, {
|
|
492
|
+
...stored,
|
|
493
|
+
replace: route.upload.replace
|
|
294
494
|
});
|
|
295
|
-
await
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
495
|
+
const head = await headObjectOrNotFound(route.client, bucket, key);
|
|
496
|
+
return finalizeConfirmedObject(route.client, bucket, key, {
|
|
497
|
+
config,
|
|
498
|
+
route,
|
|
499
|
+
stored,
|
|
500
|
+
head,
|
|
501
|
+
acl: route.upload.acl
|
|
299
502
|
});
|
|
300
|
-
const head = await headObjectOrNotFound(config.client, bucket, key);
|
|
301
|
-
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
|
|
302
|
-
const fileName = parseFileName(head.ContentDisposition);
|
|
303
|
-
const context = {
|
|
304
|
-
request,
|
|
305
|
-
key,
|
|
306
|
-
bucket,
|
|
307
|
-
contentType: head.ContentType,
|
|
308
|
-
contentLength: requireContentLength(head),
|
|
309
|
-
eTag: head.ETag?.replace(/"/g, ""),
|
|
310
|
-
metadata: head.Metadata,
|
|
311
|
-
acl,
|
|
312
|
-
fileName,
|
|
313
|
-
versionId: head.VersionId,
|
|
314
|
-
lastModified: head.LastModified?.toISOString()
|
|
315
|
-
};
|
|
316
|
-
await runLifecycleHook(config.upload?.onConfirmed, context);
|
|
317
|
-
return {
|
|
318
|
-
key,
|
|
319
|
-
bucket,
|
|
320
|
-
contentType: context.contentType,
|
|
321
|
-
contentLength: context.contentLength,
|
|
322
|
-
eTag: context.eTag,
|
|
323
|
-
metadata: context.metadata ?? {},
|
|
324
|
-
acl,
|
|
325
|
-
fileName,
|
|
326
|
-
versionId: context.versionId,
|
|
327
|
-
lastModified: context.lastModified
|
|
328
|
-
};
|
|
329
503
|
}
|
|
330
504
|
var confirm = createS3Endpoint(
|
|
331
505
|
S3_API_ROUTES.uploadConfirm,
|
|
332
506
|
{ method: "POST", body: confirmBodySchema },
|
|
333
507
|
async (ctx) => {
|
|
334
|
-
assertFeatureEnabled(ctx.context.config, "upload");
|
|
335
508
|
return handleConfirm(ctx.context.config, ctx.body, ctx.context.request);
|
|
336
509
|
}
|
|
337
510
|
);
|
|
338
511
|
async function handleDelete(config, input, request) {
|
|
339
|
-
const { key, bucket } = await
|
|
512
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
513
|
+
config,
|
|
514
|
+
input,
|
|
340
515
|
request,
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
key,
|
|
347
|
-
bucket
|
|
348
|
-
});
|
|
349
|
-
await headObjectOrNotFound(config.client, bucket, key);
|
|
350
|
-
await config.client.send(
|
|
516
|
+
"delete"
|
|
517
|
+
);
|
|
518
|
+
await runHook(route.delete.guard, stored);
|
|
519
|
+
await headObjectOrNotFound(route.client, bucket, key);
|
|
520
|
+
await route.client.send(
|
|
351
521
|
new DeleteObjectCommand({ Bucket: bucket, Key: key })
|
|
352
522
|
);
|
|
353
|
-
await runLifecycleHook(
|
|
523
|
+
await runLifecycleHook(route.delete.onDeleted, stored, config);
|
|
354
524
|
return { success: true, bucket, key };
|
|
355
525
|
}
|
|
356
526
|
var deleteObject = createS3Endpoint(
|
|
357
527
|
S3_API_ROUTES.delete,
|
|
358
528
|
{ method: "DELETE", query: deleteQuerySchema },
|
|
359
529
|
async (ctx) => {
|
|
360
|
-
assertFeatureEnabled(ctx.context.config, "delete");
|
|
361
530
|
return handleDelete(ctx.context.config, ctx.query, ctx.context.request);
|
|
362
531
|
}
|
|
363
532
|
);
|
|
533
|
+
async function deleteOne(config, routeName, key, request) {
|
|
534
|
+
const { route, bucket, stored } = await openStoredTarget(
|
|
535
|
+
config,
|
|
536
|
+
{ route: routeName, key },
|
|
537
|
+
request,
|
|
538
|
+
"delete"
|
|
539
|
+
);
|
|
540
|
+
await runHook(route.delete.guard, stored);
|
|
541
|
+
await headObjectOrNotFound(route.client, bucket, stored.key);
|
|
542
|
+
await route.client.send(
|
|
543
|
+
new DeleteObjectCommand({ Bucket: bucket, Key: stored.key })
|
|
544
|
+
);
|
|
545
|
+
await runLifecycleHook(route.delete.onDeleted, stored, config);
|
|
546
|
+
}
|
|
547
|
+
var deleteBatch = createS3Endpoint(
|
|
548
|
+
S3_API_ROUTES.deleteBatch,
|
|
549
|
+
{ method: "POST", body: deleteBatchBodySchema },
|
|
550
|
+
async (ctx) => {
|
|
551
|
+
const config = ctx.context.config;
|
|
552
|
+
const request = ctx.context.request;
|
|
553
|
+
const results = [];
|
|
554
|
+
for (const key of ctx.body.keys) {
|
|
555
|
+
try {
|
|
556
|
+
await deleteOne(config, ctx.body.route, key, request);
|
|
557
|
+
results.push({ key, success: true });
|
|
558
|
+
} catch (err) {
|
|
559
|
+
results.push({
|
|
560
|
+
key,
|
|
561
|
+
success: false,
|
|
562
|
+
error: isDimahS3Error(err) ? { code: err.code ?? "INTERNAL_ERROR", message: err.message } : { code: "INTERNAL_ERROR", message: "Internal server error" }
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return { results };
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
function fileUrl(request, basePath, query) {
|
|
570
|
+
const url = new URL(request.url);
|
|
571
|
+
const prefix = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
572
|
+
url.pathname = `${prefix}${S3_API_ROUTES.file}`;
|
|
573
|
+
url.search = "";
|
|
574
|
+
url.searchParams.set("route", query.route);
|
|
575
|
+
url.searchParams.set("key", query.key);
|
|
576
|
+
if (query.fileName) url.searchParams.set("fileName", query.fileName);
|
|
577
|
+
if (query.disposition) url.searchParams.set("disposition", query.disposition);
|
|
578
|
+
return url.toString();
|
|
579
|
+
}
|
|
364
580
|
async function handleDownload(config, input, request) {
|
|
365
|
-
const { key, bucket } = await
|
|
581
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
582
|
+
config,
|
|
583
|
+
input,
|
|
366
584
|
request,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
585
|
+
"download"
|
|
586
|
+
);
|
|
587
|
+
const resolved = await route.download.resolve?.({
|
|
588
|
+
...stored,
|
|
589
|
+
fileName: input.fileName,
|
|
590
|
+
disposition: input.disposition ?? route.download.disposition
|
|
370
591
|
});
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
592
|
+
const fileName = resolved?.fileName ?? input.fileName;
|
|
593
|
+
const disposition = resolved?.disposition ?? input.disposition ?? route.download.disposition ?? "attachment";
|
|
594
|
+
const expiresIn = normalizeExpiresIn(
|
|
595
|
+
resolved?.expiresIn ?? route.download.expiresIn,
|
|
596
|
+
config.maxExpiresIn
|
|
597
|
+
);
|
|
598
|
+
await runHook(route.download.guard, {
|
|
599
|
+
...stored,
|
|
600
|
+
fileName,
|
|
601
|
+
disposition
|
|
378
602
|
});
|
|
379
|
-
await headObjectOrNotFound(
|
|
380
|
-
const
|
|
381
|
-
|
|
603
|
+
await headObjectOrNotFound(route.client, bucket, key);
|
|
604
|
+
const mode = route.download.mode ?? "presign";
|
|
605
|
+
const contentDisposition = fileName ? buildContentDisposition(fileName, disposition) : disposition;
|
|
606
|
+
const url = mode === "proxy" ? fileUrl(request, config.basePath ?? "/api/s3", {
|
|
607
|
+
route: input.route,
|
|
608
|
+
key,
|
|
609
|
+
fileName,
|
|
610
|
+
disposition
|
|
611
|
+
}) : await getSignedUrl(
|
|
612
|
+
route.client,
|
|
382
613
|
new GetObjectCommand({
|
|
383
614
|
Bucket: bucket,
|
|
384
615
|
Key: key,
|
|
385
|
-
ResponseContentDisposition:
|
|
616
|
+
ResponseContentDisposition: contentDisposition
|
|
386
617
|
}),
|
|
387
618
|
{ expiresIn }
|
|
388
619
|
);
|
|
389
|
-
await runLifecycleHook(
|
|
390
|
-
|
|
391
|
-
|
|
620
|
+
await runLifecycleHook(
|
|
621
|
+
route.download.onPresigned,
|
|
622
|
+
{
|
|
623
|
+
...stored,
|
|
624
|
+
fileName,
|
|
625
|
+
url,
|
|
626
|
+
expiresIn: mode === "proxy" ? 0 : expiresIn
|
|
627
|
+
},
|
|
628
|
+
config
|
|
629
|
+
);
|
|
630
|
+
return {
|
|
392
631
|
bucket,
|
|
393
|
-
|
|
632
|
+
key,
|
|
394
633
|
url,
|
|
395
|
-
expiresIn
|
|
396
|
-
|
|
397
|
-
|
|
634
|
+
expiresIn: mode === "proxy" ? 0 : expiresIn,
|
|
635
|
+
mode,
|
|
636
|
+
disposition
|
|
637
|
+
};
|
|
398
638
|
}
|
|
399
639
|
var download = createS3Endpoint(
|
|
400
640
|
S3_API_ROUTES.download,
|
|
401
641
|
{ method: "GET", query: downloadQuerySchema },
|
|
402
642
|
async (ctx) => {
|
|
403
|
-
assertFeatureEnabled(ctx.context.config, "download");
|
|
404
643
|
return handleDownload(ctx.context.config, ctx.query, ctx.context.request);
|
|
405
644
|
}
|
|
406
645
|
);
|
|
407
|
-
|
|
408
|
-
|
|
646
|
+
function catalogUpload(upload2) {
|
|
647
|
+
if (!isEnabled(upload2)) return { enabled: false };
|
|
648
|
+
return {
|
|
649
|
+
enabled: true,
|
|
650
|
+
fileTypes: upload2.fileTypes,
|
|
651
|
+
maxFileSize: upload2.maxFileSize,
|
|
652
|
+
multipart: isEnabled(upload2.multipart),
|
|
653
|
+
...upload2.checksum ? { checksum: true } : {},
|
|
654
|
+
...upload2.replace ? { replace: upload2.replace } : {}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function catalogDownload(download2) {
|
|
658
|
+
if (!isEnabled(download2)) return { enabled: false };
|
|
659
|
+
return {
|
|
660
|
+
enabled: true,
|
|
661
|
+
...download2.disposition ? { disposition: download2.disposition } : {},
|
|
662
|
+
...download2.mode ? { mode: download2.mode } : {}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function buildRouteCatalog(config) {
|
|
666
|
+
const routes = {};
|
|
667
|
+
for (const [name, route] of Object.entries(config.routes)) {
|
|
668
|
+
routes[name] = {
|
|
669
|
+
upload: catalogUpload(route.upload),
|
|
670
|
+
download: catalogDownload(route.download),
|
|
671
|
+
delete: isEnabled(route.delete) ? { enabled: true } : { enabled: false }
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return { routes };
|
|
675
|
+
}
|
|
676
|
+
var catalog = createS3Endpoint(
|
|
677
|
+
S3_API_ROUTES.catalog,
|
|
678
|
+
{ method: "GET" },
|
|
679
|
+
async (ctx) => {
|
|
680
|
+
return buildRouteCatalog(ctx.context.config);
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
async function handleFile(config, input, request) {
|
|
684
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
685
|
+
config,
|
|
686
|
+
input,
|
|
409
687
|
request,
|
|
410
|
-
|
|
411
|
-
|
|
688
|
+
"download"
|
|
689
|
+
);
|
|
690
|
+
const resolved = await route.download.resolve?.({
|
|
691
|
+
...stored,
|
|
692
|
+
fileName: input.fileName,
|
|
693
|
+
disposition: input.disposition ?? route.download.disposition
|
|
412
694
|
});
|
|
413
|
-
const
|
|
414
|
-
|
|
695
|
+
const fileName = resolved?.fileName ?? input.fileName;
|
|
696
|
+
const disposition = resolved?.disposition ?? input.disposition ?? route.download.disposition ?? "attachment";
|
|
697
|
+
await runHook(route.download.guard, {
|
|
698
|
+
...stored,
|
|
699
|
+
fileName,
|
|
700
|
+
disposition
|
|
701
|
+
});
|
|
702
|
+
await headObjectOrNotFound(route.client, bucket, key);
|
|
703
|
+
const result = await sendOrObjectNotFound(
|
|
704
|
+
() => route.client.send(
|
|
705
|
+
new GetObjectCommand({
|
|
706
|
+
Bucket: bucket,
|
|
707
|
+
Key: key,
|
|
708
|
+
ResponseContentDisposition: fileName ? buildContentDisposition(fileName, disposition) : `${disposition}`
|
|
709
|
+
})
|
|
710
|
+
)
|
|
711
|
+
);
|
|
712
|
+
const body = result.Body;
|
|
713
|
+
if (!body) throw errors.objectNotFound();
|
|
714
|
+
const stream = "transformToWebStream" in body && typeof body.transformToWebStream === "function" ? body.transformToWebStream() : body;
|
|
715
|
+
const headers = new Headers();
|
|
716
|
+
headers.set("Content-Type", result.ContentType ?? "application/octet-stream");
|
|
717
|
+
headers.set(
|
|
718
|
+
"Content-Disposition",
|
|
719
|
+
fileName ? buildContentDisposition(fileName, disposition) : `${disposition}`
|
|
720
|
+
);
|
|
721
|
+
if (typeof result.ContentLength === "number") {
|
|
722
|
+
headers.set("Content-Length", String(result.ContentLength));
|
|
723
|
+
}
|
|
724
|
+
return new Response(stream, { headers });
|
|
725
|
+
}
|
|
726
|
+
var file = createS3Endpoint(
|
|
727
|
+
S3_API_ROUTES.file,
|
|
728
|
+
{ method: "GET", query: fileQuerySchema },
|
|
729
|
+
async (ctx) => {
|
|
730
|
+
return handleFile(ctx.context.config, ctx.query, ctx.context.request);
|
|
731
|
+
}
|
|
732
|
+
);
|
|
733
|
+
async function handleAbort(config, input, request) {
|
|
734
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
735
|
+
config,
|
|
736
|
+
input,
|
|
415
737
|
request,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
738
|
+
"multipart"
|
|
739
|
+
);
|
|
740
|
+
const uploadId = input.uploadId;
|
|
741
|
+
await runHook(route.upload.multipart.guard, {
|
|
742
|
+
...stored,
|
|
743
|
+
uploadId,
|
|
744
|
+
action: "abort"
|
|
419
745
|
});
|
|
420
746
|
await sendOrObjectNotFound(
|
|
421
|
-
() =>
|
|
747
|
+
() => route.client.send(
|
|
422
748
|
new AbortMultipartUploadCommand({
|
|
423
749
|
Bucket: bucket,
|
|
424
750
|
Key: key,
|
|
@@ -426,52 +752,68 @@ async function handleAbort(config, input, request) {
|
|
|
426
752
|
})
|
|
427
753
|
)
|
|
428
754
|
);
|
|
429
|
-
await runLifecycleHook(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
755
|
+
await runLifecycleHook(
|
|
756
|
+
route.upload.multipart.onAbort,
|
|
757
|
+
{
|
|
758
|
+
...stored,
|
|
759
|
+
uploadId
|
|
760
|
+
},
|
|
761
|
+
config
|
|
762
|
+
);
|
|
435
763
|
return { bucket, key, uploadId, aborted: true };
|
|
436
764
|
}
|
|
437
765
|
var multipartAbort = createS3Endpoint(
|
|
438
766
|
S3_API_ROUTES.multipartAbort,
|
|
439
767
|
{ method: "POST", body: multipartAbortBodySchema },
|
|
440
768
|
async (ctx) => {
|
|
441
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
442
769
|
return handleAbort(ctx.context.config, ctx.body, ctx.context.request);
|
|
443
770
|
}
|
|
444
771
|
);
|
|
445
772
|
async function handleComplete(config, input, request) {
|
|
446
|
-
const { key, bucket } = await
|
|
773
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
774
|
+
config,
|
|
775
|
+
input,
|
|
447
776
|
request,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
});
|
|
777
|
+
"multipart"
|
|
778
|
+
);
|
|
451
779
|
const uploadId = input.uploadId;
|
|
452
|
-
const parts =
|
|
780
|
+
const parts = [
|
|
781
|
+
...new Set(input.parts.map(({ partNumber }) => partNumber))
|
|
782
|
+
].sort((a, b) => a - b);
|
|
453
783
|
const partRefs = parts.map((partNumber) => ({ partNumber }));
|
|
454
|
-
await runHook(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
784
|
+
await runHook(route.upload.multipart.guard, {
|
|
785
|
+
...stored,
|
|
786
|
+
uploadId,
|
|
787
|
+
action: "complete"
|
|
788
|
+
});
|
|
789
|
+
await runHook(route.upload.confirmGuard, {
|
|
790
|
+
...stored,
|
|
458
791
|
uploadId,
|
|
459
|
-
parts: partRefs
|
|
792
|
+
parts: partRefs,
|
|
793
|
+
replace: route.upload.replace
|
|
460
794
|
});
|
|
461
|
-
const listedParts = await listAllParts(
|
|
795
|
+
const listedParts = await listAllParts(route.client, {
|
|
462
796
|
bucket,
|
|
463
797
|
key,
|
|
464
798
|
uploadId
|
|
465
799
|
});
|
|
800
|
+
let assembledBytes = 0;
|
|
466
801
|
const completeParts = parts.map((partNumber) => {
|
|
467
802
|
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
468
803
|
if (!found?.ETag) {
|
|
469
804
|
throw errors.multipartPartMissing(partNumber);
|
|
470
805
|
}
|
|
806
|
+
assembledBytes += found.Size ?? 0;
|
|
471
807
|
return { PartNumber: partNumber, ETag: found.ETag };
|
|
472
808
|
});
|
|
809
|
+
try {
|
|
810
|
+
assertWithinMaxFileSize(route.upload.maxFileSize, assembledBytes);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
await abortMultipartBestEffort(route.client, bucket, key, uploadId);
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
473
815
|
const completeResult = await sendOrObjectNotFound(
|
|
474
|
-
() =>
|
|
816
|
+
() => route.client.send(
|
|
475
817
|
new CompleteMultipartUploadCommand({
|
|
476
818
|
Bucket: bucket,
|
|
477
819
|
Key: key,
|
|
@@ -481,273 +823,290 @@ async function handleComplete(config, input, request) {
|
|
|
481
823
|
)
|
|
482
824
|
);
|
|
483
825
|
const head = await headObjectAfterMultipartComplete(
|
|
484
|
-
|
|
826
|
+
route.client,
|
|
485
827
|
bucket,
|
|
486
828
|
key
|
|
487
829
|
);
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
await runLifecycleHook(config.multipart?.onComplete, {
|
|
497
|
-
request,
|
|
498
|
-
key,
|
|
499
|
-
bucket,
|
|
830
|
+
const confirmed = await finalizeConfirmedObject(route.client, bucket, key, {
|
|
831
|
+
config,
|
|
832
|
+
route,
|
|
833
|
+
stored,
|
|
834
|
+
head: {
|
|
835
|
+
...head,
|
|
836
|
+
ETag: head.ETag ?? completeResult.ETag
|
|
837
|
+
},
|
|
500
838
|
uploadId,
|
|
501
|
-
|
|
502
|
-
contentType,
|
|
503
|
-
eTag,
|
|
504
|
-
metadata,
|
|
505
|
-
acl,
|
|
506
|
-
fileName,
|
|
507
|
-
versionId,
|
|
508
|
-
lastModified
|
|
839
|
+
acl: route.upload.acl
|
|
509
840
|
});
|
|
510
|
-
return {
|
|
511
|
-
bucket,
|
|
512
|
-
key,
|
|
513
|
-
uploadId,
|
|
514
|
-
contentLength,
|
|
515
|
-
contentType,
|
|
516
|
-
eTag,
|
|
517
|
-
metadata,
|
|
518
|
-
acl,
|
|
519
|
-
fileName,
|
|
520
|
-
versionId,
|
|
521
|
-
lastModified
|
|
522
|
-
};
|
|
841
|
+
return { ...confirmed, uploadId };
|
|
523
842
|
}
|
|
524
843
|
var multipartComplete = createS3Endpoint(
|
|
525
844
|
S3_API_ROUTES.multipartComplete,
|
|
526
845
|
{ method: "POST", body: multipartCompleteBodySchema },
|
|
527
846
|
async (ctx) => {
|
|
528
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
529
847
|
return handleComplete(ctx.context.config, ctx.body, ctx.context.request);
|
|
530
848
|
}
|
|
531
849
|
);
|
|
532
850
|
async function handleMultipartInit(config, input, request) {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
await runHook(config.multipart?.initGuard, {
|
|
851
|
+
const fileSize = Math.floor(input.fileSize);
|
|
852
|
+
const fileName = input.fileName;
|
|
853
|
+
const { route, key, bucket, metadata, acl, stored } = await openUploadTarget(
|
|
854
|
+
config,
|
|
855
|
+
{
|
|
856
|
+
route: input.route,
|
|
857
|
+
fileName,
|
|
858
|
+
fileSize,
|
|
859
|
+
contentType: input.contentType,
|
|
860
|
+
metadata: input.metadata
|
|
861
|
+
},
|
|
546
862
|
request,
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
863
|
+
"multipart"
|
|
864
|
+
);
|
|
865
|
+
await runHook(route.upload.guard, {
|
|
866
|
+
...stored,
|
|
867
|
+
file: {
|
|
868
|
+
name: fileName,
|
|
869
|
+
size: fileSize,
|
|
870
|
+
type: input.contentType
|
|
871
|
+
},
|
|
872
|
+
metadata,
|
|
873
|
+
clientMetadata: input.metadata,
|
|
552
874
|
acl,
|
|
553
|
-
|
|
875
|
+
replace: route.upload.replace
|
|
554
876
|
});
|
|
555
|
-
const { UploadId } = await
|
|
877
|
+
const { UploadId } = await route.client.send(
|
|
556
878
|
new CreateMultipartUploadCommand({
|
|
557
879
|
Bucket: bucket,
|
|
558
880
|
Key: key,
|
|
559
881
|
ContentType: input.contentType,
|
|
560
|
-
ContentDisposition:
|
|
561
|
-
Metadata:
|
|
882
|
+
ContentDisposition: buildContentDisposition(fileName),
|
|
883
|
+
Metadata: metadata,
|
|
562
884
|
ACL: acl
|
|
563
885
|
})
|
|
564
886
|
);
|
|
565
887
|
if (!UploadId) {
|
|
566
888
|
throw errors.internalError();
|
|
567
889
|
}
|
|
568
|
-
await runLifecycleHook(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
890
|
+
await runLifecycleHook(
|
|
891
|
+
route.upload.multipart.onInit,
|
|
892
|
+
{
|
|
893
|
+
...stored,
|
|
894
|
+
uploadId: UploadId,
|
|
895
|
+
file: {
|
|
896
|
+
name: fileName,
|
|
897
|
+
size: fileSize,
|
|
898
|
+
type: input.contentType
|
|
899
|
+
},
|
|
900
|
+
metadata,
|
|
901
|
+
clientMetadata: input.metadata,
|
|
902
|
+
acl,
|
|
903
|
+
replace: route.upload.replace
|
|
904
|
+
},
|
|
905
|
+
config
|
|
906
|
+
);
|
|
579
907
|
return { bucket, key, uploadId: UploadId };
|
|
580
908
|
}
|
|
581
909
|
var multipartInit = createS3Endpoint(
|
|
582
910
|
S3_API_ROUTES.multipartInit,
|
|
583
911
|
{ method: "POST", body: multipartInitBodySchema },
|
|
584
912
|
async (ctx) => {
|
|
585
|
-
|
|
586
|
-
ctx.setStatus(201);
|
|
587
|
-
return handleMultipartInit(
|
|
913
|
+
const result = await handleMultipartInit(
|
|
588
914
|
ctx.context.config,
|
|
589
915
|
ctx.body,
|
|
590
916
|
ctx.context.request
|
|
591
917
|
);
|
|
918
|
+
ctx.setStatus(201);
|
|
919
|
+
return result;
|
|
592
920
|
}
|
|
593
921
|
);
|
|
594
922
|
async function handleListParts(config, input, request) {
|
|
595
|
-
const { key, bucket } = await
|
|
923
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
924
|
+
config,
|
|
925
|
+
input,
|
|
596
926
|
request,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
});
|
|
927
|
+
"multipart"
|
|
928
|
+
);
|
|
600
929
|
const uploadId = input.uploadId;
|
|
601
|
-
await runHook(
|
|
602
|
-
|
|
603
|
-
key,
|
|
604
|
-
bucket,
|
|
605
|
-
uploadId
|
|
606
|
-
});
|
|
607
|
-
const listed = await listAllParts(config.client, { bucket, key, uploadId });
|
|
608
|
-
const parts = listed.map((p) => ({
|
|
609
|
-
partNumber: p.PartNumber ?? 0,
|
|
610
|
-
size: p.Size ?? 0,
|
|
611
|
-
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
612
|
-
}));
|
|
613
|
-
await runLifecycleHook(config.multipart?.onList, {
|
|
614
|
-
request,
|
|
615
|
-
key,
|
|
616
|
-
bucket,
|
|
930
|
+
await runHook(route.upload.multipart.guard, {
|
|
931
|
+
...stored,
|
|
617
932
|
uploadId,
|
|
618
|
-
|
|
933
|
+
action: "list"
|
|
619
934
|
});
|
|
935
|
+
const listed = await listAllParts(route.client, { bucket, key, uploadId });
|
|
936
|
+
const parts = listed.flatMap((p) => {
|
|
937
|
+
if (p.PartNumber == null || p.PartNumber < 1) return [];
|
|
938
|
+
return [
|
|
939
|
+
{
|
|
940
|
+
partNumber: p.PartNumber,
|
|
941
|
+
size: p.Size ?? 0,
|
|
942
|
+
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
943
|
+
}
|
|
944
|
+
];
|
|
945
|
+
});
|
|
946
|
+
await runLifecycleHook(
|
|
947
|
+
route.upload.multipart.onList,
|
|
948
|
+
{
|
|
949
|
+
...stored,
|
|
950
|
+
uploadId,
|
|
951
|
+
parts
|
|
952
|
+
},
|
|
953
|
+
config
|
|
954
|
+
);
|
|
620
955
|
return { parts };
|
|
621
956
|
}
|
|
622
957
|
var multipartListParts = createS3Endpoint(
|
|
623
958
|
S3_API_ROUTES.multipartListParts,
|
|
624
959
|
{ method: "GET", query: multipartListPartsQuerySchema },
|
|
625
960
|
async (ctx) => {
|
|
626
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
627
961
|
return handleListParts(ctx.context.config, ctx.query, ctx.context.request);
|
|
628
962
|
}
|
|
629
963
|
);
|
|
630
964
|
async function handleSignPart(config, input, request) {
|
|
631
|
-
const { key, bucket } = await
|
|
965
|
+
const { route, key, bucket, stored } = await openStoredTarget(
|
|
966
|
+
config,
|
|
967
|
+
input,
|
|
632
968
|
request,
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
});
|
|
969
|
+
"multipart"
|
|
970
|
+
);
|
|
636
971
|
const uploadId = input.uploadId;
|
|
637
972
|
const partNumber = input.partNumber;
|
|
638
|
-
const expiresIn = normalizeExpiresIn(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
973
|
+
const expiresIn = normalizeExpiresIn(
|
|
974
|
+
route.upload.expiresIn,
|
|
975
|
+
config.maxExpiresIn
|
|
976
|
+
);
|
|
977
|
+
const partSize = Math.floor(input.partSize);
|
|
978
|
+
await runHook(route.upload.multipart.guard, {
|
|
979
|
+
...stored,
|
|
644
980
|
uploadId,
|
|
981
|
+
action: "part",
|
|
645
982
|
partNumber,
|
|
646
|
-
partSize
|
|
983
|
+
partSize
|
|
647
984
|
});
|
|
648
|
-
const
|
|
649
|
-
|
|
985
|
+
const maxFileSize = route.upload.maxFileSize;
|
|
986
|
+
assertWithinMaxFileSize(maxFileSize, partSize);
|
|
987
|
+
if (typeof maxFileSize === "number") {
|
|
988
|
+
const listed = await listAllParts(route.client, { bucket, key, uploadId });
|
|
989
|
+
assertWithinMaxFileSize(
|
|
990
|
+
maxFileSize,
|
|
991
|
+
listedPartsByteSize(listed, partNumber) + partSize
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
const url = await getSignedUrl(
|
|
995
|
+
route.client,
|
|
650
996
|
new UploadPartCommand({
|
|
651
997
|
Bucket: bucket,
|
|
652
998
|
Key: key,
|
|
653
999
|
UploadId: uploadId,
|
|
654
1000
|
PartNumber: partNumber,
|
|
655
|
-
|
|
1001
|
+
ContentLength: partSize
|
|
656
1002
|
}),
|
|
657
1003
|
{
|
|
658
1004
|
expiresIn,
|
|
659
|
-
|
|
1005
|
+
signableHeaders: /* @__PURE__ */ new Set(["content-length"])
|
|
660
1006
|
}
|
|
661
1007
|
);
|
|
662
1008
|
return {
|
|
663
|
-
|
|
1009
|
+
url,
|
|
664
1010
|
partNumber,
|
|
665
1011
|
uploadId,
|
|
666
1012
|
bucket,
|
|
667
1013
|
expiresIn,
|
|
668
|
-
|
|
1014
|
+
partSize
|
|
669
1015
|
};
|
|
670
1016
|
}
|
|
671
1017
|
var multipartPart = createS3Endpoint(
|
|
672
1018
|
S3_API_ROUTES.multipartPart,
|
|
673
1019
|
{ method: "POST", body: multipartSignPartBodySchema },
|
|
674
1020
|
async (ctx) => {
|
|
675
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
676
1021
|
return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
|
|
677
1022
|
}
|
|
678
1023
|
);
|
|
679
|
-
function
|
|
1024
|
+
function objectUserMetadata(metadata) {
|
|
680
1025
|
if (!metadata) return {};
|
|
681
|
-
const
|
|
1026
|
+
const fields = {};
|
|
682
1027
|
for (const [k, v] of Object.entries(metadata)) {
|
|
683
|
-
|
|
1028
|
+
fields[`x-amz-meta-${k}`] = v;
|
|
684
1029
|
}
|
|
685
|
-
return
|
|
1030
|
+
return fields;
|
|
686
1031
|
}
|
|
687
1032
|
async function handleUpload(config, input, request) {
|
|
688
|
-
const
|
|
1033
|
+
const fileSize = Math.floor(input.fileSize);
|
|
1034
|
+
const fileName = input.fileName;
|
|
1035
|
+
const { route, key, bucket, metadata, acl, stored } = await openUploadTarget(
|
|
1036
|
+
config,
|
|
1037
|
+
{
|
|
1038
|
+
route: input.route,
|
|
1039
|
+
fileName,
|
|
1040
|
+
fileSize,
|
|
1041
|
+
contentType: input.contentType,
|
|
1042
|
+
metadata: input.metadata
|
|
1043
|
+
},
|
|
689
1044
|
request,
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
696
|
-
const acl = resolveRequestAcl(config.upload, input.acl);
|
|
697
|
-
const contentType = input.contentType ?? "application/octet-stream";
|
|
698
|
-
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
|
|
699
|
-
if (config.upload?.requireFileSize && fileSize === null) {
|
|
700
|
-
throw errors.fileSizeRequiredUpload();
|
|
1045
|
+
"upload"
|
|
1046
|
+
);
|
|
1047
|
+
const method = route.upload.method ?? "POST";
|
|
1048
|
+
if (route.upload.checksum && !input.checksum) {
|
|
1049
|
+
throw errors.validationError("Checksum is required for this route");
|
|
701
1050
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1051
|
+
if (method === "POST" && fileSize > S3_MAX_POST_OBJECT_BYTES) {
|
|
1052
|
+
throw errors.payloadTooLarge(
|
|
1053
|
+
`POST uploads cannot exceed ${S3_MAX_POST_OBJECT_BYTES} bytes. Enable upload.multipart or set upload.method to "PUT".`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const expiresIn = normalizeExpiresIn(
|
|
1057
|
+
route.upload.expiresIn,
|
|
1058
|
+
config.maxExpiresIn
|
|
1059
|
+
);
|
|
1060
|
+
const contentType = input.contentType ?? "application/octet-stream";
|
|
1061
|
+
const hookCtx = {
|
|
1062
|
+
...stored,
|
|
1063
|
+
file: {
|
|
1064
|
+
name: fileName,
|
|
1065
|
+
size: fileSize,
|
|
1066
|
+
type: input.contentType
|
|
1067
|
+
},
|
|
1068
|
+
metadata,
|
|
1069
|
+
clientMetadata: input.metadata,
|
|
709
1070
|
acl,
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
1071
|
+
replace: route.upload.replace
|
|
1072
|
+
};
|
|
1073
|
+
await runHook(route.upload.guard, hookCtx);
|
|
713
1074
|
if (method === "PUT") {
|
|
714
1075
|
const putHeaders = {
|
|
715
1076
|
"Content-Type": contentType,
|
|
716
|
-
...
|
|
1077
|
+
...objectUserMetadata(metadata),
|
|
1078
|
+
"Content-Disposition": buildContentDisposition(fileName),
|
|
1079
|
+
...input.checksum ? { "x-amz-checksum-sha256": input.checksum } : {}
|
|
717
1080
|
};
|
|
718
|
-
if (input.fileName) {
|
|
719
|
-
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
720
|
-
input.fileName
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
1081
|
const url2 = await getSignedUrl(
|
|
724
|
-
|
|
1082
|
+
route.client,
|
|
725
1083
|
new PutObjectCommand({
|
|
726
1084
|
Bucket: bucket,
|
|
727
1085
|
Key: key,
|
|
728
1086
|
ContentType: contentType,
|
|
729
1087
|
ACL: acl,
|
|
730
|
-
Metadata:
|
|
731
|
-
|
|
732
|
-
|
|
1088
|
+
Metadata: metadata,
|
|
1089
|
+
ContentDisposition: buildContentDisposition(fileName),
|
|
1090
|
+
ContentLength: fileSize,
|
|
1091
|
+
...input.checksum ? { ChecksumSHA256: input.checksum, ChecksumAlgorithm: "SHA256" } : {}
|
|
733
1092
|
}),
|
|
734
1093
|
{
|
|
735
1094
|
expiresIn,
|
|
736
|
-
|
|
1095
|
+
signableHeaders: /* @__PURE__ */ new Set([
|
|
1096
|
+
"content-length",
|
|
1097
|
+
...input.checksum ? ["x-amz-checksum-sha256"] : []
|
|
1098
|
+
])
|
|
737
1099
|
}
|
|
738
1100
|
);
|
|
739
|
-
await runLifecycleHook(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
url: url2,
|
|
749
|
-
expiresIn
|
|
750
|
-
});
|
|
1101
|
+
await runLifecycleHook(
|
|
1102
|
+
route.upload.onPresigned,
|
|
1103
|
+
{
|
|
1104
|
+
...hookCtx,
|
|
1105
|
+
url: url2,
|
|
1106
|
+
expiresIn
|
|
1107
|
+
},
|
|
1108
|
+
config
|
|
1109
|
+
);
|
|
751
1110
|
return {
|
|
752
1111
|
bucket,
|
|
753
1112
|
key,
|
|
@@ -757,39 +1116,32 @@ async function handleUpload(config, input, request) {
|
|
|
757
1116
|
method: "PUT"
|
|
758
1117
|
};
|
|
759
1118
|
}
|
|
760
|
-
const fields = {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
const rangeMin = fileSize ?? 1;
|
|
770
|
-
const rangeMax = fileSize ?? void 0;
|
|
1119
|
+
const fields = {
|
|
1120
|
+
acl,
|
|
1121
|
+
"Content-Type": contentType,
|
|
1122
|
+
"Content-Disposition": buildContentDisposition(fileName),
|
|
1123
|
+
...objectUserMetadata(metadata),
|
|
1124
|
+
...input.checksum ? { "x-amz-checksum-sha256": input.checksum } : {}
|
|
1125
|
+
};
|
|
771
1126
|
const { url, fields: signedFields } = await createPresignedPost(
|
|
772
|
-
|
|
1127
|
+
route.client,
|
|
773
1128
|
{
|
|
774
1129
|
Bucket: bucket,
|
|
775
1130
|
Key: key,
|
|
776
|
-
Conditions:
|
|
1131
|
+
Conditions: [["content-length-range", fileSize, fileSize]],
|
|
777
1132
|
Fields: fields,
|
|
778
1133
|
Expires: expiresIn
|
|
779
1134
|
}
|
|
780
1135
|
);
|
|
781
|
-
await runLifecycleHook(
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
url,
|
|
791
|
-
expiresIn
|
|
792
|
-
});
|
|
1136
|
+
await runLifecycleHook(
|
|
1137
|
+
route.upload.onPresigned,
|
|
1138
|
+
{
|
|
1139
|
+
...hookCtx,
|
|
1140
|
+
url,
|
|
1141
|
+
expiresIn
|
|
1142
|
+
},
|
|
1143
|
+
config
|
|
1144
|
+
);
|
|
793
1145
|
return {
|
|
794
1146
|
bucket,
|
|
795
1147
|
key,
|
|
@@ -803,7 +1155,6 @@ var upload = createS3Endpoint(
|
|
|
803
1155
|
S3_API_ROUTES.upload,
|
|
804
1156
|
{ method: "POST", body: uploadBodySchema },
|
|
805
1157
|
async (ctx) => {
|
|
806
|
-
assertFeatureEnabled(ctx.context.config, "upload");
|
|
807
1158
|
return handleUpload(ctx.context.config, ctx.body, ctx.context.request);
|
|
808
1159
|
}
|
|
809
1160
|
);
|
|
@@ -813,7 +1164,10 @@ var coreEndpoints = {
|
|
|
813
1164
|
upload,
|
|
814
1165
|
confirm,
|
|
815
1166
|
download,
|
|
1167
|
+
catalog,
|
|
1168
|
+
file,
|
|
816
1169
|
delete: deleteObject,
|
|
1170
|
+
deleteBatch,
|
|
817
1171
|
multipartInit,
|
|
818
1172
|
multipartPart,
|
|
819
1173
|
multipartListParts,
|