@dimah-s3/server 1.0.2 → 1.1.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/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/index.js
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema,
|
|
1
|
+
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, isDimahS3Error, deleteBatchBodySchema, downloadQuerySchema, fileQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, resolveStoredFileName, S3_MAX_EXPIRES_IN, buildContentDisposition, S3_MAX_POST_OBJECT_BYTES, normalizeS3ApiBasePath, S3_API_BASE_PATH, matchesFileTypes, S3_DEFAULT_EXPIRES_IN, normalizeObjectKey, sanitizeFileName, ROUTE_NAME_PATTERN } from '@dimah-s3/core';
|
|
2
2
|
export { DimahS3Error, S3_ERROR_CODES, isAPIError, isDimahS3Error, isS3ErrorCode } from '@dimah-s3/core';
|
|
3
|
-
import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand,
|
|
3
|
+
import { PutObjectCommand, DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, ListPartsCommand, AbortMultipartUploadCommand, CompleteMultipartUploadCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
4
4
|
import { createMiddleware, createEndpoint, createRouter, toResponse } from 'better-call';
|
|
5
5
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
6
6
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
7
7
|
|
|
8
8
|
// src/dimah-s3.ts
|
|
9
|
+
|
|
10
|
+
// src/helpers/request.ts
|
|
11
|
+
function requestFromHeaders(headers) {
|
|
12
|
+
return new Request("http://dimah-s3.local", { headers });
|
|
13
|
+
}
|
|
14
|
+
async function deleteObjectBestEffort(client, bucket, key) {
|
|
15
|
+
try {
|
|
16
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function abortMultipartBestEffort(client, bucket, key, uploadId) {
|
|
21
|
+
try {
|
|
22
|
+
await client.send(
|
|
23
|
+
new AbortMultipartUploadCommand({
|
|
24
|
+
Bucket: bucket,
|
|
25
|
+
Key: key,
|
|
26
|
+
UploadId: uploadId
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
9
32
|
var errors = {
|
|
10
33
|
notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
|
|
11
34
|
unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
|
|
@@ -22,10 +45,18 @@ var errors = {
|
|
|
22
45
|
params: { feature }
|
|
23
46
|
}),
|
|
24
47
|
invalidKey: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.INVALID_KEY),
|
|
25
|
-
|
|
26
|
-
code: S3_ERROR_CODES.
|
|
27
|
-
message: S3_ERROR_CODES.
|
|
28
|
-
params: {
|
|
48
|
+
unknownRoute: (route2) => DimahS3Error.from("NOT_FOUND", {
|
|
49
|
+
code: S3_ERROR_CODES.UNKNOWN_ROUTE.code,
|
|
50
|
+
message: S3_ERROR_CODES.UNKNOWN_ROUTE.message,
|
|
51
|
+
params: { route: route2 }
|
|
52
|
+
}),
|
|
53
|
+
fileTypeNotAllowed: (fileName, contentType) => DimahS3Error.from("BAD_REQUEST", {
|
|
54
|
+
code: S3_ERROR_CODES.FILE_TYPE_NOT_ALLOWED.code,
|
|
55
|
+
message: S3_ERROR_CODES.FILE_TYPE_NOT_ALLOWED.message,
|
|
56
|
+
params: {
|
|
57
|
+
fileName,
|
|
58
|
+
...contentType ? { contentType } : {}
|
|
59
|
+
}
|
|
29
60
|
}),
|
|
30
61
|
s3NetworkError: (code) => DimahS3Error.from("BAD_GATEWAY", {
|
|
31
62
|
code: S3_ERROR_CODES.S3_NETWORK_ERROR.code,
|
|
@@ -35,11 +66,6 @@ var errors = {
|
|
|
35
66
|
),
|
|
36
67
|
params: { code }
|
|
37
68
|
}),
|
|
38
|
-
fileSizeRequiredUpload: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD),
|
|
39
|
-
fileSizeRequiredMultipart: () => DimahS3Error.from(
|
|
40
|
-
"BAD_REQUEST",
|
|
41
|
-
S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
|
|
42
|
-
),
|
|
43
69
|
multipartPartMissing: (partNumber) => DimahS3Error.from("BAD_REQUEST", {
|
|
44
70
|
code: S3_ERROR_CODES.MULTIPART_PART_MISSING.code,
|
|
45
71
|
message: S3_ERROR_CODES.MULTIPART_PART_MISSING.message.replaceAll(
|
|
@@ -52,6 +78,10 @@ var errors = {
|
|
|
52
78
|
...S3_ERROR_CODES.PAYLOAD_TOO_LARGE,
|
|
53
79
|
message
|
|
54
80
|
}),
|
|
81
|
+
quotaExceeded: (message = S3_ERROR_CODES.QUOTA_EXCEEDED.message) => DimahS3Error.from("PAYLOAD_TOO_LARGE", {
|
|
82
|
+
...S3_ERROR_CODES.QUOTA_EXCEEDED,
|
|
83
|
+
message
|
|
84
|
+
}),
|
|
55
85
|
validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
|
|
56
86
|
code: S3_ERROR_CODES.VALIDATION_ERROR.code,
|
|
57
87
|
message
|
|
@@ -71,119 +101,364 @@ async function sendOrObjectNotFound(send) {
|
|
|
71
101
|
throw err;
|
|
72
102
|
}
|
|
73
103
|
}
|
|
74
|
-
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
75
|
-
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
76
|
-
const controller = new AbortController();
|
|
77
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
-
try {
|
|
79
|
-
const result = await s3.send(
|
|
80
|
-
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
81
|
-
{ abortSignal: controller.signal }
|
|
82
|
-
);
|
|
83
|
-
const isPublic = result.Grants?.some(
|
|
84
|
-
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
85
|
-
);
|
|
86
|
-
return isPublic ? "public-read" : "private";
|
|
87
|
-
} catch {
|
|
88
|
-
return void 0;
|
|
89
|
-
} finally {
|
|
90
|
-
clearTimeout(timeout);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/helpers/resolve-request-acl.ts
|
|
95
|
-
function resolveRequestAcl(policy, requested) {
|
|
96
|
-
if (policy?.acl) return policy.acl;
|
|
97
|
-
if (policy?.allowClientAcl && requested) return requested;
|
|
98
|
-
return "private";
|
|
99
|
-
}
|
|
100
104
|
function normalizeExpiresIn(value, maxExpiresIn = S3_MAX_EXPIRES_IN) {
|
|
101
105
|
const n = Number(value);
|
|
102
106
|
const expires = Number.isFinite(n) && n > 0 ? Math.floor(n) : S3_DEFAULT_EXPIRES_IN;
|
|
103
107
|
const max = Number.isFinite(maxExpiresIn) && maxExpiresIn > 0 ? Math.floor(maxExpiresIn) : S3_MAX_EXPIRES_IN;
|
|
104
108
|
return Math.min(expires, max);
|
|
105
109
|
}
|
|
110
|
+
|
|
111
|
+
// src/helpers/logger.ts
|
|
112
|
+
var defaultLogger = {
|
|
113
|
+
error: (message, ...args) => {
|
|
114
|
+
console.error("[dimah-s3]", message, ...args);
|
|
115
|
+
},
|
|
116
|
+
warn: (message, ...args) => {
|
|
117
|
+
console.warn("[dimah-s3]", message, ...args);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function resolveLogger(logger) {
|
|
121
|
+
if (logger === false) return {};
|
|
122
|
+
if (!logger) return defaultLogger;
|
|
123
|
+
return logger;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/helpers/hooks.ts
|
|
127
|
+
function mapGuardError(err) {
|
|
128
|
+
if (isAPIError(err)) throw err;
|
|
129
|
+
if (err instanceof Error && err.message.trim()) {
|
|
130
|
+
throw new DimahS3Error("FORBIDDEN", {
|
|
131
|
+
message: err.message,
|
|
132
|
+
cause: err
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
throw errors.forbidden();
|
|
136
|
+
}
|
|
106
137
|
async function runHook(hook, context) {
|
|
107
138
|
if (!hook) return;
|
|
108
139
|
try {
|
|
109
140
|
await hook(context);
|
|
110
141
|
} catch (err) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
mapGuardError(err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function runObjectHook(hook, context) {
|
|
146
|
+
if (!hook) return;
|
|
147
|
+
try {
|
|
148
|
+
return await hook(context);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
mapGuardError(err);
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
|
-
async function runLifecycleHook(hook, context) {
|
|
153
|
+
async function runLifecycleHook(hook, context, config) {
|
|
122
154
|
if (!hook) return;
|
|
123
155
|
try {
|
|
124
156
|
await hook(context);
|
|
125
157
|
} catch (err) {
|
|
126
158
|
if (isAPIError(err)) throw err;
|
|
127
|
-
|
|
159
|
+
resolveLogger(config?.logger).error?.("lifecycle hook failed", err);
|
|
160
|
+
config?.onError?.(err, { request: context.request });
|
|
128
161
|
throw errors.internalError();
|
|
129
162
|
}
|
|
130
163
|
}
|
|
131
164
|
|
|
132
|
-
// src/helpers/
|
|
133
|
-
|
|
134
|
-
|
|
165
|
+
// src/helpers/previous-key.ts
|
|
166
|
+
var DIMAH_PREVIOUS_KEY_META = "dimah-previous-key";
|
|
167
|
+
function stripPreviousKeyMeta(metadata) {
|
|
168
|
+
if (!metadata) return {};
|
|
169
|
+
const { [DIMAH_PREVIOUS_KEY_META]: _omit, ...rest } = metadata;
|
|
170
|
+
return rest;
|
|
171
|
+
}
|
|
172
|
+
function previousKeyFromMetadata(metadata) {
|
|
173
|
+
const value = metadata?.[DIMAH_PREVIOUS_KEY_META]?.trim();
|
|
174
|
+
return value || void 0;
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
// src/helpers/resolve-target.ts
|
|
138
178
|
function assertSafeObjectKey(key) {
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
if (trimmed.includes("\\") || trimmed.includes("\0")) {
|
|
144
|
-
throw errors.invalidKey();
|
|
145
|
-
}
|
|
146
|
-
const parts = trimmed.split("/");
|
|
147
|
-
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
148
|
-
throw errors.invalidKey();
|
|
149
|
-
}
|
|
150
|
-
return parts.join("/");
|
|
179
|
+
const normalized = normalizeObjectKey(key);
|
|
180
|
+
if (!normalized) throw errors.invalidKey();
|
|
181
|
+
return normalized;
|
|
151
182
|
}
|
|
152
183
|
function applyPrefix(prefix, key) {
|
|
153
184
|
const p = assertSafeObjectKey(prefix);
|
|
154
185
|
const k = assertSafeObjectKey(key);
|
|
155
|
-
if (k
|
|
186
|
+
if (k.startsWith(`${p}/`)) return k;
|
|
187
|
+
if (k === p) throw errors.invalidKey();
|
|
156
188
|
return `${p}/${k}`;
|
|
157
189
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
function nestKeyUnderPrefix(keyPrefix, key) {
|
|
191
|
+
const safe = assertSafeObjectKey(key);
|
|
192
|
+
if (keyPrefix === false) return safe;
|
|
193
|
+
return applyPrefix(keyPrefix, safe);
|
|
194
|
+
}
|
|
195
|
+
function assertStoredKey(key, keyPrefix) {
|
|
196
|
+
const safe = assertSafeObjectKey(key);
|
|
197
|
+
if (keyPrefix === false) return safe;
|
|
198
|
+
const root = assertSafeObjectKey(keyPrefix);
|
|
199
|
+
if (safe.startsWith(`${root}/`)) return safe;
|
|
200
|
+
throw errors.invalidKey();
|
|
201
|
+
}
|
|
202
|
+
function objectKeyFileName(fileName) {
|
|
203
|
+
const sanitized = sanitizeFileName(fileName);
|
|
204
|
+
const leaf = sanitized.split("/").filter((part) => part.length > 0).at(-1);
|
|
205
|
+
if (!leaf || leaf === "." || leaf === "..") return "file";
|
|
206
|
+
return leaf;
|
|
207
|
+
}
|
|
208
|
+
function generateObjectKey(folder, fileName) {
|
|
209
|
+
const leaf = `${crypto.randomUUID()}/${objectKeyFileName(fileName)}`;
|
|
210
|
+
if (folder === false) return assertSafeObjectKey(leaf);
|
|
211
|
+
return applyPrefix(folder, leaf);
|
|
212
|
+
}
|
|
213
|
+
function resolveAcl(info, route2) {
|
|
214
|
+
return info?.acl ?? route2.upload.acl ?? "private";
|
|
215
|
+
}
|
|
216
|
+
async function resolveUploadTarget(route2, context) {
|
|
217
|
+
const bucket = route2.bucket;
|
|
218
|
+
const objectContext = {
|
|
219
|
+
...context,
|
|
220
|
+
bucket,
|
|
221
|
+
keyPrefix: route2.keyPrefix
|
|
222
|
+
};
|
|
223
|
+
const info = await runObjectHook(route2.upload.object, objectContext) ?? void 0;
|
|
224
|
+
const key = info?.key ? nestKeyUnderPrefix(route2.keyPrefix, info.key) : generateObjectKey(
|
|
225
|
+
info?.folder != null && info.folder !== "" ? nestKeyUnderPrefix(route2.keyPrefix, info.folder) : route2.keyPrefix === false ? false : route2.keyPrefix,
|
|
226
|
+
context.file.name
|
|
227
|
+
);
|
|
228
|
+
const metadata = { ...info?.metadata };
|
|
229
|
+
if (info?.previousKey) {
|
|
230
|
+
metadata[DIMAH_PREVIOUS_KEY_META] = nestKeyUnderPrefix(
|
|
231
|
+
route2.keyPrefix,
|
|
232
|
+
info.previousKey
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
key,
|
|
237
|
+
bucket,
|
|
238
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : info?.metadata,
|
|
239
|
+
acl: resolveAcl(info, route2)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function resolveStoredTarget(route2, key) {
|
|
243
|
+
return {
|
|
244
|
+
key: assertStoredKey(key, route2.keyPrefix),
|
|
245
|
+
bucket: route2.bucket
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function route(config) {
|
|
249
|
+
return config;
|
|
250
|
+
}
|
|
251
|
+
function assertRouteName(name) {
|
|
252
|
+
if (!ROUTE_NAME_PATTERN.test(name)) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`dimahS3 route "${name}" is invalid. Use a letter followed by up to 63 letters, digits, underscores, or hyphens.`
|
|
255
|
+
);
|
|
161
256
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/helpers/features.ts
|
|
260
|
+
function isEnabled(feature) {
|
|
261
|
+
return feature.enabled;
|
|
262
|
+
}
|
|
263
|
+
function resolveKeyPrefix(name, value) {
|
|
264
|
+
if (value === false) return false;
|
|
265
|
+
const normalized = normalizeObjectKey(value ?? name);
|
|
266
|
+
if (!normalized) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`dimahS3 route "${name}": keyPrefix is not a valid object-key prefix.`
|
|
269
|
+
);
|
|
165
270
|
}
|
|
166
|
-
return
|
|
271
|
+
return normalized;
|
|
167
272
|
}
|
|
168
|
-
function
|
|
169
|
-
if (
|
|
170
|
-
if (
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
273
|
+
function isFeatureOn(value, defaultOn = false) {
|
|
274
|
+
if (value === false) return false;
|
|
275
|
+
if (value === void 0) return defaultOn;
|
|
276
|
+
if (value === true) return true;
|
|
277
|
+
if ("enabled" in value && typeof value.enabled === "boolean") {
|
|
278
|
+
return value.enabled;
|
|
174
279
|
}
|
|
175
|
-
return
|
|
280
|
+
return true;
|
|
176
281
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
function normalizeFeature(value, defaultOn = false) {
|
|
283
|
+
if (value === false) return { enabled: false };
|
|
284
|
+
if (value === void 0) {
|
|
285
|
+
if (!defaultOn) return { enabled: false };
|
|
286
|
+
return { enabled: true };
|
|
287
|
+
}
|
|
288
|
+
if (value === true) return { enabled: true };
|
|
289
|
+
return { ...value, enabled: true };
|
|
290
|
+
}
|
|
291
|
+
function skippedPluginIds(plugins) {
|
|
292
|
+
const ids = /* @__PURE__ */ new Set();
|
|
293
|
+
if (!plugins) return ids;
|
|
294
|
+
for (const [id, enabled] of Object.entries(plugins)) {
|
|
295
|
+
if (enabled === false) ids.add(id);
|
|
296
|
+
}
|
|
297
|
+
return ids;
|
|
298
|
+
}
|
|
299
|
+
function splitUpload(upload2) {
|
|
300
|
+
if (upload2 === void 0 || typeof upload2 === "boolean") {
|
|
301
|
+
return { upload: upload2, multipart: void 0 };
|
|
302
|
+
}
|
|
303
|
+
const { multipart, ...rest } = upload2;
|
|
304
|
+
return { upload: rest, multipart };
|
|
305
|
+
}
|
|
306
|
+
function normalizeRoute(name, route2, instance) {
|
|
307
|
+
assertRouteName(name);
|
|
308
|
+
const client = route2.client ?? instance.client;
|
|
309
|
+
const bucket = route2.bucket ?? instance.bucket;
|
|
310
|
+
if (!client) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`dimahS3 route "${name}": set client on the route or on dimahS3().`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (!bucket) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`dimahS3 route "${name}": set bucket on the route or on dimahS3().`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const { upload: uploadInput, multipart: multipartInput } = splitUpload(
|
|
321
|
+
route2.upload
|
|
322
|
+
);
|
|
323
|
+
const upload2 = normalizeFeature(uploadInput);
|
|
324
|
+
const download2 = normalizeFeature(route2.download);
|
|
325
|
+
const deleteFeature = normalizeFeature(route2.delete);
|
|
326
|
+
const multipart = normalizeFeature(multipartInput);
|
|
327
|
+
if (!upload2.enabled && !download2.enabled && !deleteFeature.enabled) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`dimahS3 route "${name}": enable upload, download, or delete.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const resolvedUpload = upload2.enabled ? { ...upload2, multipart } : { enabled: false, multipart: { enabled: false } };
|
|
333
|
+
if (resolvedUpload.enabled) {
|
|
334
|
+
const maxFileSize = resolvedUpload.maxFileSize;
|
|
335
|
+
if (maxFileSize != null && (!Number.isFinite(maxFileSize) || maxFileSize <= 0)) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`dimahS3 route "${name}": maxFileSize must be a positive number.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
name,
|
|
343
|
+
client,
|
|
344
|
+
bucket,
|
|
345
|
+
keyPrefix: resolveKeyPrefix(name, route2.keyPrefix),
|
|
346
|
+
guard: route2.guard,
|
|
347
|
+
skippedPluginIds: skippedPluginIds(route2.plugins),
|
|
348
|
+
upload: resolvedUpload,
|
|
349
|
+
download: download2,
|
|
350
|
+
delete: deleteFeature
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function normalizeRoutes(config) {
|
|
354
|
+
const entries = Object.entries(config.routes);
|
|
355
|
+
if (entries.length === 0) {
|
|
356
|
+
throw new Error("dimahS3: at least one route is required.");
|
|
357
|
+
}
|
|
358
|
+
const routes = {};
|
|
359
|
+
for (const [name, route2] of entries) {
|
|
360
|
+
routes[name] = normalizeRoute(name, route2, config);
|
|
361
|
+
}
|
|
362
|
+
assertDistinctKeyPrefixes(routes);
|
|
363
|
+
return routes;
|
|
364
|
+
}
|
|
365
|
+
function prefixesOverlap(a, b) {
|
|
366
|
+
return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
367
|
+
}
|
|
368
|
+
function assertDistinctKeyPrefixes(routes) {
|
|
369
|
+
const named = Object.entries(routes).filter(
|
|
370
|
+
(entry) => entry[1].keyPrefix !== false
|
|
371
|
+
);
|
|
372
|
+
for (let i = 0; i < named.length; i++) {
|
|
373
|
+
for (let j = i + 1; j < named.length; j++) {
|
|
374
|
+
const [nameA, routeA] = named[i];
|
|
375
|
+
const [nameB, routeB] = named[j];
|
|
376
|
+
if (prefixesOverlap(routeA.keyPrefix, routeB.keyPrefix)) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`dimahS3: routes "${nameA}" and "${nameB}" have overlapping keyPrefix values ("${routeA.keyPrefix}" and "${routeB.keyPrefix}").`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function assertWithinMaxFileSize(maxFileSize, size) {
|
|
385
|
+
if (typeof maxFileSize === "number" && size > maxFileSize) {
|
|
386
|
+
throw errors.payloadTooLarge();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function assertDeclaredConstraints(constraints, input) {
|
|
390
|
+
if (constraints?.fileTypes?.length && !matchesFileTypes(input.fileName, input.contentType, constraints.fileTypes)) {
|
|
391
|
+
throw errors.fileTypeNotAllowed(input.fileName, input.contentType);
|
|
392
|
+
}
|
|
393
|
+
assertWithinMaxFileSize(constraints?.maxFileSize, input.fileSize);
|
|
394
|
+
}
|
|
395
|
+
function assertVerifiedConstraints(constraints, input) {
|
|
396
|
+
assertWithinMaxFileSize(constraints?.maxFileSize, input.contentLength);
|
|
397
|
+
const name = input.fileName ?? "";
|
|
398
|
+
if (constraints?.fileTypes?.length && !matchesFileTypes(name, input.contentType, constraints.fileTypes)) {
|
|
399
|
+
throw errors.fileTypeNotAllowed(name || "unknown", input.contentType);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/helpers/resolve-route.ts
|
|
404
|
+
function getResolvedRoute(config, name) {
|
|
405
|
+
const route2 = config.routes[name];
|
|
406
|
+
if (!route2) {
|
|
407
|
+
throw errors.unknownRoute(name);
|
|
408
|
+
}
|
|
409
|
+
return route2;
|
|
410
|
+
}
|
|
411
|
+
function featureIsEnabled(route2, feature) {
|
|
412
|
+
if (feature === "multipart") return route2.upload.multipart.enabled;
|
|
413
|
+
return route2[feature].enabled;
|
|
414
|
+
}
|
|
415
|
+
function assertFeatureEnabled(route2, feature) {
|
|
416
|
+
if (!featureIsEnabled(route2, feature)) {
|
|
417
|
+
throw errors.featureDisabled(feature);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function openRoute(config, name, request, feature) {
|
|
421
|
+
const route2 = getResolvedRoute(config, name);
|
|
422
|
+
assertFeatureEnabled(route2, feature);
|
|
423
|
+
await runHook(route2.guard, { request, route: route2.name });
|
|
424
|
+
return route2;
|
|
425
|
+
}
|
|
426
|
+
function storedObjectContext(request, route2, key) {
|
|
427
|
+
return { request, route: route2.name, key, bucket: route2.bucket };
|
|
428
|
+
}
|
|
429
|
+
async function openUploadTarget(config, input, request, feature) {
|
|
430
|
+
const route2 = await openRoute(config, input.route, request, feature);
|
|
431
|
+
const fileSize = Math.floor(input.fileSize);
|
|
432
|
+
const fileName = input.fileName;
|
|
433
|
+
assertDeclaredConstraints(route2.upload, {
|
|
434
|
+
fileName,
|
|
435
|
+
fileSize,
|
|
436
|
+
contentType: input.contentType
|
|
437
|
+
});
|
|
438
|
+
const target = await resolveUploadTarget(route2, {
|
|
439
|
+
request,
|
|
440
|
+
route: route2.name,
|
|
441
|
+
file: {
|
|
442
|
+
name: fileName,
|
|
443
|
+
size: fileSize,
|
|
444
|
+
type: input.contentType
|
|
445
|
+
},
|
|
446
|
+
clientMetadata: input.metadata
|
|
185
447
|
});
|
|
186
|
-
return {
|
|
448
|
+
return {
|
|
449
|
+
route: route2,
|
|
450
|
+
...target,
|
|
451
|
+
stored: storedObjectContext(request, route2, target.key)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async function openStoredTarget(config, input, request, feature) {
|
|
455
|
+
const route2 = await openRoute(config, input.route, request, feature);
|
|
456
|
+
const target = resolveStoredTarget(route2, input.key);
|
|
457
|
+
return {
|
|
458
|
+
route: route2,
|
|
459
|
+
...target,
|
|
460
|
+
stored: storedObjectContext(request, route2, target.key)
|
|
461
|
+
};
|
|
187
462
|
}
|
|
188
463
|
async function headObjectOrNotFound(client, bucket, key) {
|
|
189
464
|
return sendOrObjectNotFound(
|
|
@@ -195,7 +470,7 @@ async function headObjectAfterMultipartComplete(client, bucket, key) {
|
|
|
195
470
|
() => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
|
|
196
471
|
);
|
|
197
472
|
let head = await send();
|
|
198
|
-
for (let attempt = 0; attempt < 4 &&
|
|
473
|
+
for (let attempt = 0; attempt < 4 && typeof head.ContentLength !== "number"; attempt++) {
|
|
199
474
|
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
200
475
|
head = await send();
|
|
201
476
|
}
|
|
@@ -228,61 +503,126 @@ async function listAllParts(client, input) {
|
|
|
228
503
|
}
|
|
229
504
|
return parts;
|
|
230
505
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return config[feature]?.enabled === true;
|
|
506
|
+
function listedPartsByteSize(parts, excludePartNumber) {
|
|
507
|
+
let total = 0;
|
|
508
|
+
for (const part of parts) {
|
|
509
|
+
if (excludePartNumber !== void 0 && part.PartNumber === excludePartNumber) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
total += part.Size ?? 0;
|
|
513
|
+
}
|
|
514
|
+
return total;
|
|
241
515
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
516
|
+
async function finalizeConfirmedObject(client, bucket, key, input) {
|
|
517
|
+
const { config, route: route2, stored, head, uploadId, acl } = input;
|
|
518
|
+
const contentLength = requireContentLength(head);
|
|
519
|
+
const fileName = resolveStoredFileName(head.ContentDisposition, key);
|
|
520
|
+
const rawMetadata = head.Metadata ?? {};
|
|
521
|
+
try {
|
|
522
|
+
assertVerifiedConstraints(route2.upload, {
|
|
523
|
+
fileName,
|
|
524
|
+
contentType: head.ContentType,
|
|
525
|
+
contentLength
|
|
526
|
+
});
|
|
527
|
+
} catch (err) {
|
|
528
|
+
await deleteObjectBestEffort(client, bucket, key);
|
|
529
|
+
throw err;
|
|
249
530
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
531
|
+
const metadata = stripPreviousKeyMeta(rawMetadata);
|
|
532
|
+
const context = {
|
|
533
|
+
...stored,
|
|
534
|
+
contentType: head.ContentType,
|
|
535
|
+
contentLength,
|
|
536
|
+
eTag: head.ETag?.replace(/"/g, ""),
|
|
537
|
+
metadata,
|
|
538
|
+
acl,
|
|
539
|
+
fileName,
|
|
540
|
+
versionId: head.VersionId,
|
|
541
|
+
lastModified: head.LastModified?.toISOString(),
|
|
542
|
+
uploadId
|
|
543
|
+
};
|
|
544
|
+
try {
|
|
545
|
+
await runLifecycleHook(route2.upload.onConfirmed, context, config);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
await deleteObjectBestEffort(client, bucket, key);
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
const previousKey = previousKeyFromMetadata(rawMetadata);
|
|
551
|
+
if (previousKey && previousKey !== key) {
|
|
552
|
+
try {
|
|
553
|
+
const safe = assertStoredKey(previousKey, route2.keyPrefix);
|
|
554
|
+
await deleteObjectBestEffort(client, bucket, safe);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
262
557
|
}
|
|
263
|
-
return next;
|
|
264
|
-
}
|
|
265
|
-
function normalizeFeatures(config) {
|
|
266
558
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
559
|
+
key,
|
|
560
|
+
bucket,
|
|
561
|
+
contentType: context.contentType,
|
|
562
|
+
contentLength: context.contentLength,
|
|
563
|
+
eTag: context.eTag,
|
|
564
|
+
metadata: context.metadata ?? {},
|
|
565
|
+
acl,
|
|
566
|
+
fileName,
|
|
567
|
+
versionId: context.versionId,
|
|
568
|
+
lastModified: context.lastModified
|
|
271
569
|
};
|
|
272
570
|
}
|
|
273
|
-
function assertExclusiveBucketFlags(config) {
|
|
274
|
-
if (config.allowClientBucket && config.buckets?.length) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
"dimahS3: set either allowClientBucket or buckets, not both. allowClientBucket honors any client-sent bucket; buckets is an allowlist."
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
571
|
|
|
281
|
-
// src/
|
|
282
|
-
function
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
572
|
+
// src/put.ts
|
|
573
|
+
function toBytes(body) {
|
|
574
|
+
if (typeof body === "string") return new TextEncoder().encode(body);
|
|
575
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
576
|
+
return body;
|
|
577
|
+
}
|
|
578
|
+
async function putObject(config, input) {
|
|
579
|
+
const body = toBytes(input.body);
|
|
580
|
+
const request = requestFromHeaders(input.headers);
|
|
581
|
+
await runHook(config.guard, { request });
|
|
582
|
+
const fileSize = body.byteLength;
|
|
583
|
+
const { route: route2, key, bucket, metadata, acl, stored } = await openUploadTarget(
|
|
584
|
+
config,
|
|
585
|
+
{
|
|
586
|
+
route: input.route,
|
|
587
|
+
fileName: input.fileName,
|
|
588
|
+
fileSize,
|
|
589
|
+
contentType: input.contentType,
|
|
590
|
+
metadata: input.metadata
|
|
591
|
+
},
|
|
592
|
+
request,
|
|
593
|
+
"upload"
|
|
594
|
+
);
|
|
595
|
+
await runHook(route2.upload.guard, {
|
|
596
|
+
...stored,
|
|
597
|
+
file: {
|
|
598
|
+
name: input.fileName,
|
|
599
|
+
size: fileSize,
|
|
600
|
+
type: input.contentType
|
|
601
|
+
},
|
|
602
|
+
metadata,
|
|
603
|
+
clientMetadata: input.metadata,
|
|
604
|
+
acl,
|
|
605
|
+
replace: route2.upload.replace
|
|
606
|
+
});
|
|
607
|
+
await route2.client.send(
|
|
608
|
+
new PutObjectCommand({
|
|
609
|
+
Bucket: bucket,
|
|
610
|
+
Key: key,
|
|
611
|
+
Body: body,
|
|
612
|
+
ContentType: input.contentType,
|
|
613
|
+
ContentLength: fileSize,
|
|
614
|
+
Metadata: metadata,
|
|
615
|
+
ACL: acl
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
const head = await headObjectOrNotFound(route2.client, bucket, key);
|
|
619
|
+
return finalizeConfirmedObject(route2.client, bucket, key, {
|
|
620
|
+
config,
|
|
621
|
+
route: route2,
|
|
622
|
+
stored,
|
|
623
|
+
head,
|
|
624
|
+
acl
|
|
625
|
+
});
|
|
286
626
|
}
|
|
287
627
|
var s3ContextMiddleware = createMiddleware(async (ctx) => {
|
|
288
628
|
const injected = ctx.context;
|
|
@@ -330,138 +670,269 @@ var createS3Endpoint = ((pathOrOptions, optionsOrHandler, maybeHandler) => {
|
|
|
330
670
|
|
|
331
671
|
// src/api/routes/confirm.ts
|
|
332
672
|
async function handleConfirm(config, input, request) {
|
|
333
|
-
const { key, bucket } = await
|
|
673
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
674
|
+
config,
|
|
675
|
+
input,
|
|
334
676
|
request,
|
|
335
|
-
|
|
336
|
-
|
|
677
|
+
"upload"
|
|
678
|
+
);
|
|
679
|
+
await runHook(route2.upload.confirmGuard, {
|
|
680
|
+
...stored,
|
|
681
|
+
replace: route2.upload.replace
|
|
337
682
|
});
|
|
338
|
-
await
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
683
|
+
const head = await headObjectOrNotFound(route2.client, bucket, key);
|
|
684
|
+
return finalizeConfirmedObject(route2.client, bucket, key, {
|
|
685
|
+
config,
|
|
686
|
+
route: route2,
|
|
687
|
+
stored,
|
|
688
|
+
head,
|
|
689
|
+
acl: route2.upload.acl
|
|
342
690
|
});
|
|
343
|
-
const head = await headObjectOrNotFound(config.client, bucket, key);
|
|
344
|
-
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
|
|
345
|
-
const fileName = parseFileName(head.ContentDisposition);
|
|
346
|
-
const context = {
|
|
347
|
-
request,
|
|
348
|
-
key,
|
|
349
|
-
bucket,
|
|
350
|
-
contentType: head.ContentType,
|
|
351
|
-
contentLength: requireContentLength(head),
|
|
352
|
-
eTag: head.ETag?.replace(/"/g, ""),
|
|
353
|
-
metadata: head.Metadata,
|
|
354
|
-
acl,
|
|
355
|
-
fileName,
|
|
356
|
-
versionId: head.VersionId,
|
|
357
|
-
lastModified: head.LastModified?.toISOString()
|
|
358
|
-
};
|
|
359
|
-
await runLifecycleHook(config.upload?.onConfirmed, context);
|
|
360
|
-
return {
|
|
361
|
-
key,
|
|
362
|
-
bucket,
|
|
363
|
-
contentType: context.contentType,
|
|
364
|
-
contentLength: context.contentLength,
|
|
365
|
-
eTag: context.eTag,
|
|
366
|
-
metadata: context.metadata ?? {},
|
|
367
|
-
acl,
|
|
368
|
-
fileName,
|
|
369
|
-
versionId: context.versionId,
|
|
370
|
-
lastModified: context.lastModified
|
|
371
|
-
};
|
|
372
691
|
}
|
|
373
692
|
var confirm = createS3Endpoint(
|
|
374
693
|
S3_API_ROUTES.uploadConfirm,
|
|
375
694
|
{ method: "POST", body: confirmBodySchema },
|
|
376
695
|
async (ctx) => {
|
|
377
|
-
assertFeatureEnabled(ctx.context.config, "upload");
|
|
378
696
|
return handleConfirm(ctx.context.config, ctx.body, ctx.context.request);
|
|
379
697
|
}
|
|
380
698
|
);
|
|
381
699
|
async function handleDelete(config, input, request) {
|
|
382
|
-
const { key, bucket } = await
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
bucket: input.bucket
|
|
386
|
-
});
|
|
387
|
-
await runHook(config.delete?.guard, {
|
|
700
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
701
|
+
config,
|
|
702
|
+
input,
|
|
388
703
|
request,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
await headObjectOrNotFound(
|
|
393
|
-
await
|
|
704
|
+
"delete"
|
|
705
|
+
);
|
|
706
|
+
await runHook(route2.delete.guard, stored);
|
|
707
|
+
await headObjectOrNotFound(route2.client, bucket, key);
|
|
708
|
+
await route2.client.send(
|
|
394
709
|
new DeleteObjectCommand({ Bucket: bucket, Key: key })
|
|
395
710
|
);
|
|
396
|
-
await runLifecycleHook(
|
|
711
|
+
await runLifecycleHook(route2.delete.onDeleted, stored, config);
|
|
397
712
|
return { success: true, bucket, key };
|
|
398
713
|
}
|
|
399
714
|
var deleteObject = createS3Endpoint(
|
|
400
715
|
S3_API_ROUTES.delete,
|
|
401
716
|
{ method: "DELETE", query: deleteQuerySchema },
|
|
402
717
|
async (ctx) => {
|
|
403
|
-
assertFeatureEnabled(ctx.context.config, "delete");
|
|
404
718
|
return handleDelete(ctx.context.config, ctx.query, ctx.context.request);
|
|
405
719
|
}
|
|
406
720
|
);
|
|
721
|
+
async function deleteOne(config, routeName, key, request) {
|
|
722
|
+
const { route: route2, bucket, stored } = await openStoredTarget(
|
|
723
|
+
config,
|
|
724
|
+
{ route: routeName, key },
|
|
725
|
+
request,
|
|
726
|
+
"delete"
|
|
727
|
+
);
|
|
728
|
+
await runHook(route2.delete.guard, stored);
|
|
729
|
+
await headObjectOrNotFound(route2.client, bucket, stored.key);
|
|
730
|
+
await route2.client.send(
|
|
731
|
+
new DeleteObjectCommand({ Bucket: bucket, Key: stored.key })
|
|
732
|
+
);
|
|
733
|
+
await runLifecycleHook(route2.delete.onDeleted, stored, config);
|
|
734
|
+
}
|
|
735
|
+
var deleteBatch = createS3Endpoint(
|
|
736
|
+
S3_API_ROUTES.deleteBatch,
|
|
737
|
+
{ method: "POST", body: deleteBatchBodySchema },
|
|
738
|
+
async (ctx) => {
|
|
739
|
+
const config = ctx.context.config;
|
|
740
|
+
const request = ctx.context.request;
|
|
741
|
+
const results = [];
|
|
742
|
+
for (const key of ctx.body.keys) {
|
|
743
|
+
try {
|
|
744
|
+
await deleteOne(config, ctx.body.route, key, request);
|
|
745
|
+
results.push({ key, success: true });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
results.push({
|
|
748
|
+
key,
|
|
749
|
+
success: false,
|
|
750
|
+
error: isDimahS3Error(err) ? { code: err.code ?? "INTERNAL_ERROR", message: err.message } : { code: "INTERNAL_ERROR", message: "Internal server error" }
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return { results };
|
|
755
|
+
}
|
|
756
|
+
);
|
|
757
|
+
function fileUrl(request, basePath, query) {
|
|
758
|
+
const url = new URL(request.url);
|
|
759
|
+
const prefix = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
760
|
+
url.pathname = `${prefix}${S3_API_ROUTES.file}`;
|
|
761
|
+
url.search = "";
|
|
762
|
+
url.searchParams.set("route", query.route);
|
|
763
|
+
url.searchParams.set("key", query.key);
|
|
764
|
+
if (query.fileName) url.searchParams.set("fileName", query.fileName);
|
|
765
|
+
if (query.disposition) url.searchParams.set("disposition", query.disposition);
|
|
766
|
+
return url.toString();
|
|
767
|
+
}
|
|
407
768
|
async function handleDownload(config, input, request) {
|
|
408
|
-
const { key, bucket } = await
|
|
769
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
770
|
+
config,
|
|
771
|
+
input,
|
|
409
772
|
request,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
773
|
+
"download"
|
|
774
|
+
);
|
|
775
|
+
const resolved = await route2.download.resolve?.({
|
|
776
|
+
...stored,
|
|
777
|
+
fileName: input.fileName,
|
|
778
|
+
disposition: input.disposition ?? route2.download.disposition
|
|
413
779
|
});
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
780
|
+
const fileName = resolved?.fileName ?? input.fileName;
|
|
781
|
+
const disposition = resolved?.disposition ?? input.disposition ?? route2.download.disposition ?? "attachment";
|
|
782
|
+
const expiresIn = normalizeExpiresIn(
|
|
783
|
+
resolved?.expiresIn ?? route2.download.expiresIn,
|
|
784
|
+
config.maxExpiresIn
|
|
785
|
+
);
|
|
786
|
+
await runHook(route2.download.guard, {
|
|
787
|
+
...stored,
|
|
788
|
+
fileName,
|
|
789
|
+
disposition
|
|
421
790
|
});
|
|
422
|
-
await headObjectOrNotFound(
|
|
423
|
-
const
|
|
424
|
-
|
|
791
|
+
await headObjectOrNotFound(route2.client, bucket, key);
|
|
792
|
+
const mode = route2.download.mode ?? "presign";
|
|
793
|
+
const contentDisposition = fileName ? buildContentDisposition(fileName, disposition) : disposition;
|
|
794
|
+
const url = mode === "proxy" ? fileUrl(request, config.basePath ?? "/api/s3", {
|
|
795
|
+
route: input.route,
|
|
796
|
+
key,
|
|
797
|
+
fileName,
|
|
798
|
+
disposition
|
|
799
|
+
}) : await getSignedUrl(
|
|
800
|
+
route2.client,
|
|
425
801
|
new GetObjectCommand({
|
|
426
802
|
Bucket: bucket,
|
|
427
803
|
Key: key,
|
|
428
|
-
ResponseContentDisposition:
|
|
804
|
+
ResponseContentDisposition: contentDisposition
|
|
429
805
|
}),
|
|
430
806
|
{ expiresIn }
|
|
431
807
|
);
|
|
432
|
-
await runLifecycleHook(
|
|
433
|
-
|
|
434
|
-
|
|
808
|
+
await runLifecycleHook(
|
|
809
|
+
route2.download.onPresigned,
|
|
810
|
+
{
|
|
811
|
+
...stored,
|
|
812
|
+
fileName,
|
|
813
|
+
url,
|
|
814
|
+
expiresIn: mode === "proxy" ? 0 : expiresIn
|
|
815
|
+
},
|
|
816
|
+
config
|
|
817
|
+
);
|
|
818
|
+
return {
|
|
435
819
|
bucket,
|
|
436
|
-
|
|
820
|
+
key,
|
|
437
821
|
url,
|
|
438
|
-
expiresIn
|
|
439
|
-
|
|
440
|
-
|
|
822
|
+
expiresIn: mode === "proxy" ? 0 : expiresIn,
|
|
823
|
+
mode,
|
|
824
|
+
disposition
|
|
825
|
+
};
|
|
441
826
|
}
|
|
442
827
|
var download = createS3Endpoint(
|
|
443
828
|
S3_API_ROUTES.download,
|
|
444
829
|
{ method: "GET", query: downloadQuerySchema },
|
|
445
830
|
async (ctx) => {
|
|
446
|
-
assertFeatureEnabled(ctx.context.config, "download");
|
|
447
831
|
return handleDownload(ctx.context.config, ctx.query, ctx.context.request);
|
|
448
832
|
}
|
|
449
833
|
);
|
|
450
|
-
|
|
451
|
-
|
|
834
|
+
function catalogUpload(upload2) {
|
|
835
|
+
if (!isEnabled(upload2)) return { enabled: false };
|
|
836
|
+
return {
|
|
837
|
+
enabled: true,
|
|
838
|
+
fileTypes: upload2.fileTypes,
|
|
839
|
+
maxFileSize: upload2.maxFileSize,
|
|
840
|
+
multipart: isEnabled(upload2.multipart),
|
|
841
|
+
...upload2.checksum ? { checksum: true } : {},
|
|
842
|
+
...upload2.replace ? { replace: upload2.replace } : {}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function catalogDownload(download2) {
|
|
846
|
+
if (!isEnabled(download2)) return { enabled: false };
|
|
847
|
+
return {
|
|
848
|
+
enabled: true,
|
|
849
|
+
...download2.disposition ? { disposition: download2.disposition } : {},
|
|
850
|
+
...download2.mode ? { mode: download2.mode } : {}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function buildRouteCatalog(config) {
|
|
854
|
+
const routes = {};
|
|
855
|
+
for (const [name, route2] of Object.entries(config.routes)) {
|
|
856
|
+
routes[name] = {
|
|
857
|
+
upload: catalogUpload(route2.upload),
|
|
858
|
+
download: catalogDownload(route2.download),
|
|
859
|
+
delete: isEnabled(route2.delete) ? { enabled: true } : { enabled: false }
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return { routes };
|
|
863
|
+
}
|
|
864
|
+
var catalog = createS3Endpoint(
|
|
865
|
+
S3_API_ROUTES.catalog,
|
|
866
|
+
{ method: "GET" },
|
|
867
|
+
async (ctx) => {
|
|
868
|
+
return buildRouteCatalog(ctx.context.config);
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
async function handleFile(config, input, request) {
|
|
872
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
873
|
+
config,
|
|
874
|
+
input,
|
|
452
875
|
request,
|
|
453
|
-
|
|
454
|
-
|
|
876
|
+
"download"
|
|
877
|
+
);
|
|
878
|
+
const resolved = await route2.download.resolve?.({
|
|
879
|
+
...stored,
|
|
880
|
+
fileName: input.fileName,
|
|
881
|
+
disposition: input.disposition ?? route2.download.disposition
|
|
455
882
|
});
|
|
456
|
-
const
|
|
457
|
-
|
|
883
|
+
const fileName = resolved?.fileName ?? input.fileName;
|
|
884
|
+
const disposition = resolved?.disposition ?? input.disposition ?? route2.download.disposition ?? "attachment";
|
|
885
|
+
await runHook(route2.download.guard, {
|
|
886
|
+
...stored,
|
|
887
|
+
fileName,
|
|
888
|
+
disposition
|
|
889
|
+
});
|
|
890
|
+
await headObjectOrNotFound(route2.client, bucket, key);
|
|
891
|
+
const result = await sendOrObjectNotFound(
|
|
892
|
+
() => route2.client.send(
|
|
893
|
+
new GetObjectCommand({
|
|
894
|
+
Bucket: bucket,
|
|
895
|
+
Key: key,
|
|
896
|
+
ResponseContentDisposition: fileName ? buildContentDisposition(fileName, disposition) : `${disposition}`
|
|
897
|
+
})
|
|
898
|
+
)
|
|
899
|
+
);
|
|
900
|
+
const body = result.Body;
|
|
901
|
+
if (!body) throw errors.objectNotFound();
|
|
902
|
+
const stream = "transformToWebStream" in body && typeof body.transformToWebStream === "function" ? body.transformToWebStream() : body;
|
|
903
|
+
const headers = new Headers();
|
|
904
|
+
headers.set("Content-Type", result.ContentType ?? "application/octet-stream");
|
|
905
|
+
headers.set(
|
|
906
|
+
"Content-Disposition",
|
|
907
|
+
fileName ? buildContentDisposition(fileName, disposition) : `${disposition}`
|
|
908
|
+
);
|
|
909
|
+
if (typeof result.ContentLength === "number") {
|
|
910
|
+
headers.set("Content-Length", String(result.ContentLength));
|
|
911
|
+
}
|
|
912
|
+
return new Response(stream, { headers });
|
|
913
|
+
}
|
|
914
|
+
var file = createS3Endpoint(
|
|
915
|
+
S3_API_ROUTES.file,
|
|
916
|
+
{ method: "GET", query: fileQuerySchema },
|
|
917
|
+
async (ctx) => {
|
|
918
|
+
return handleFile(ctx.context.config, ctx.query, ctx.context.request);
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
async function handleAbort(config, input, request) {
|
|
922
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
923
|
+
config,
|
|
924
|
+
input,
|
|
458
925
|
request,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
926
|
+
"multipart"
|
|
927
|
+
);
|
|
928
|
+
const uploadId = input.uploadId;
|
|
929
|
+
await runHook(route2.upload.multipart.guard, {
|
|
930
|
+
...stored,
|
|
931
|
+
uploadId,
|
|
932
|
+
action: "abort"
|
|
462
933
|
});
|
|
463
934
|
await sendOrObjectNotFound(
|
|
464
|
-
() =>
|
|
935
|
+
() => route2.client.send(
|
|
465
936
|
new AbortMultipartUploadCommand({
|
|
466
937
|
Bucket: bucket,
|
|
467
938
|
Key: key,
|
|
@@ -469,52 +940,68 @@ async function handleAbort(config, input, request) {
|
|
|
469
940
|
})
|
|
470
941
|
)
|
|
471
942
|
);
|
|
472
|
-
await runLifecycleHook(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
943
|
+
await runLifecycleHook(
|
|
944
|
+
route2.upload.multipart.onAbort,
|
|
945
|
+
{
|
|
946
|
+
...stored,
|
|
947
|
+
uploadId
|
|
948
|
+
},
|
|
949
|
+
config
|
|
950
|
+
);
|
|
478
951
|
return { bucket, key, uploadId, aborted: true };
|
|
479
952
|
}
|
|
480
953
|
var multipartAbort = createS3Endpoint(
|
|
481
954
|
S3_API_ROUTES.multipartAbort,
|
|
482
955
|
{ method: "POST", body: multipartAbortBodySchema },
|
|
483
956
|
async (ctx) => {
|
|
484
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
485
957
|
return handleAbort(ctx.context.config, ctx.body, ctx.context.request);
|
|
486
958
|
}
|
|
487
959
|
);
|
|
488
960
|
async function handleComplete(config, input, request) {
|
|
489
|
-
const { key, bucket } = await
|
|
961
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
962
|
+
config,
|
|
963
|
+
input,
|
|
490
964
|
request,
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
});
|
|
965
|
+
"multipart"
|
|
966
|
+
);
|
|
494
967
|
const uploadId = input.uploadId;
|
|
495
|
-
const parts =
|
|
968
|
+
const parts = [
|
|
969
|
+
...new Set(input.parts.map(({ partNumber }) => partNumber))
|
|
970
|
+
].sort((a, b) => a - b);
|
|
496
971
|
const partRefs = parts.map((partNumber) => ({ partNumber }));
|
|
497
|
-
await runHook(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
972
|
+
await runHook(route2.upload.multipart.guard, {
|
|
973
|
+
...stored,
|
|
974
|
+
uploadId,
|
|
975
|
+
action: "complete"
|
|
976
|
+
});
|
|
977
|
+
await runHook(route2.upload.confirmGuard, {
|
|
978
|
+
...stored,
|
|
501
979
|
uploadId,
|
|
502
|
-
parts: partRefs
|
|
980
|
+
parts: partRefs,
|
|
981
|
+
replace: route2.upload.replace
|
|
503
982
|
});
|
|
504
|
-
const listedParts = await listAllParts(
|
|
983
|
+
const listedParts = await listAllParts(route2.client, {
|
|
505
984
|
bucket,
|
|
506
985
|
key,
|
|
507
986
|
uploadId
|
|
508
987
|
});
|
|
988
|
+
let assembledBytes = 0;
|
|
509
989
|
const completeParts = parts.map((partNumber) => {
|
|
510
990
|
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
511
991
|
if (!found?.ETag) {
|
|
512
992
|
throw errors.multipartPartMissing(partNumber);
|
|
513
993
|
}
|
|
994
|
+
assembledBytes += found.Size ?? 0;
|
|
514
995
|
return { PartNumber: partNumber, ETag: found.ETag };
|
|
515
996
|
});
|
|
997
|
+
try {
|
|
998
|
+
assertWithinMaxFileSize(route2.upload.maxFileSize, assembledBytes);
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
await abortMultipartBestEffort(route2.client, bucket, key, uploadId);
|
|
1001
|
+
throw err;
|
|
1002
|
+
}
|
|
516
1003
|
const completeResult = await sendOrObjectNotFound(
|
|
517
|
-
() =>
|
|
1004
|
+
() => route2.client.send(
|
|
518
1005
|
new CompleteMultipartUploadCommand({
|
|
519
1006
|
Bucket: bucket,
|
|
520
1007
|
Key: key,
|
|
@@ -524,273 +1011,290 @@ async function handleComplete(config, input, request) {
|
|
|
524
1011
|
)
|
|
525
1012
|
);
|
|
526
1013
|
const head = await headObjectAfterMultipartComplete(
|
|
527
|
-
|
|
1014
|
+
route2.client,
|
|
528
1015
|
bucket,
|
|
529
1016
|
key
|
|
530
1017
|
);
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
await runLifecycleHook(config.multipart?.onComplete, {
|
|
540
|
-
request,
|
|
541
|
-
key,
|
|
542
|
-
bucket,
|
|
1018
|
+
const confirmed = await finalizeConfirmedObject(route2.client, bucket, key, {
|
|
1019
|
+
config,
|
|
1020
|
+
route: route2,
|
|
1021
|
+
stored,
|
|
1022
|
+
head: {
|
|
1023
|
+
...head,
|
|
1024
|
+
ETag: head.ETag ?? completeResult.ETag
|
|
1025
|
+
},
|
|
543
1026
|
uploadId,
|
|
544
|
-
|
|
545
|
-
contentType,
|
|
546
|
-
eTag,
|
|
547
|
-
metadata,
|
|
548
|
-
acl,
|
|
549
|
-
fileName,
|
|
550
|
-
versionId,
|
|
551
|
-
lastModified
|
|
1027
|
+
acl: route2.upload.acl
|
|
552
1028
|
});
|
|
553
|
-
return {
|
|
554
|
-
bucket,
|
|
555
|
-
key,
|
|
556
|
-
uploadId,
|
|
557
|
-
contentLength,
|
|
558
|
-
contentType,
|
|
559
|
-
eTag,
|
|
560
|
-
metadata,
|
|
561
|
-
acl,
|
|
562
|
-
fileName,
|
|
563
|
-
versionId,
|
|
564
|
-
lastModified
|
|
565
|
-
};
|
|
1029
|
+
return { ...confirmed, uploadId };
|
|
566
1030
|
}
|
|
567
1031
|
var multipartComplete = createS3Endpoint(
|
|
568
1032
|
S3_API_ROUTES.multipartComplete,
|
|
569
1033
|
{ method: "POST", body: multipartCompleteBodySchema },
|
|
570
1034
|
async (ctx) => {
|
|
571
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
572
1035
|
return handleComplete(ctx.context.config, ctx.body, ctx.context.request);
|
|
573
1036
|
}
|
|
574
1037
|
);
|
|
575
1038
|
async function handleMultipartInit(config, input, request) {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
await runHook(config.multipart?.initGuard, {
|
|
1039
|
+
const fileSize = Math.floor(input.fileSize);
|
|
1040
|
+
const fileName = input.fileName;
|
|
1041
|
+
const { route: route2, key, bucket, metadata, acl, stored } = await openUploadTarget(
|
|
1042
|
+
config,
|
|
1043
|
+
{
|
|
1044
|
+
route: input.route,
|
|
1045
|
+
fileName,
|
|
1046
|
+
fileSize,
|
|
1047
|
+
contentType: input.contentType,
|
|
1048
|
+
metadata: input.metadata
|
|
1049
|
+
},
|
|
589
1050
|
request,
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1051
|
+
"multipart"
|
|
1052
|
+
);
|
|
1053
|
+
await runHook(route2.upload.guard, {
|
|
1054
|
+
...stored,
|
|
1055
|
+
file: {
|
|
1056
|
+
name: fileName,
|
|
1057
|
+
size: fileSize,
|
|
1058
|
+
type: input.contentType
|
|
1059
|
+
},
|
|
1060
|
+
metadata,
|
|
1061
|
+
clientMetadata: input.metadata,
|
|
595
1062
|
acl,
|
|
596
|
-
|
|
1063
|
+
replace: route2.upload.replace
|
|
597
1064
|
});
|
|
598
|
-
const { UploadId } = await
|
|
1065
|
+
const { UploadId } = await route2.client.send(
|
|
599
1066
|
new CreateMultipartUploadCommand({
|
|
600
1067
|
Bucket: bucket,
|
|
601
1068
|
Key: key,
|
|
602
1069
|
ContentType: input.contentType,
|
|
603
|
-
ContentDisposition:
|
|
604
|
-
Metadata:
|
|
1070
|
+
ContentDisposition: buildContentDisposition(fileName),
|
|
1071
|
+
Metadata: metadata,
|
|
605
1072
|
ACL: acl
|
|
606
1073
|
})
|
|
607
1074
|
);
|
|
608
1075
|
if (!UploadId) {
|
|
609
1076
|
throw errors.internalError();
|
|
610
1077
|
}
|
|
611
|
-
await runLifecycleHook(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1078
|
+
await runLifecycleHook(
|
|
1079
|
+
route2.upload.multipart.onInit,
|
|
1080
|
+
{
|
|
1081
|
+
...stored,
|
|
1082
|
+
uploadId: UploadId,
|
|
1083
|
+
file: {
|
|
1084
|
+
name: fileName,
|
|
1085
|
+
size: fileSize,
|
|
1086
|
+
type: input.contentType
|
|
1087
|
+
},
|
|
1088
|
+
metadata,
|
|
1089
|
+
clientMetadata: input.metadata,
|
|
1090
|
+
acl,
|
|
1091
|
+
replace: route2.upload.replace
|
|
1092
|
+
},
|
|
1093
|
+
config
|
|
1094
|
+
);
|
|
622
1095
|
return { bucket, key, uploadId: UploadId };
|
|
623
1096
|
}
|
|
624
1097
|
var multipartInit = createS3Endpoint(
|
|
625
1098
|
S3_API_ROUTES.multipartInit,
|
|
626
1099
|
{ method: "POST", body: multipartInitBodySchema },
|
|
627
1100
|
async (ctx) => {
|
|
628
|
-
|
|
629
|
-
ctx.setStatus(201);
|
|
630
|
-
return handleMultipartInit(
|
|
1101
|
+
const result = await handleMultipartInit(
|
|
631
1102
|
ctx.context.config,
|
|
632
1103
|
ctx.body,
|
|
633
1104
|
ctx.context.request
|
|
634
1105
|
);
|
|
1106
|
+
ctx.setStatus(201);
|
|
1107
|
+
return result;
|
|
635
1108
|
}
|
|
636
1109
|
);
|
|
637
1110
|
async function handleListParts(config, input, request) {
|
|
638
|
-
const { key, bucket } = await
|
|
1111
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
1112
|
+
config,
|
|
1113
|
+
input,
|
|
639
1114
|
request,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
});
|
|
1115
|
+
"multipart"
|
|
1116
|
+
);
|
|
643
1117
|
const uploadId = input.uploadId;
|
|
644
|
-
await runHook(
|
|
645
|
-
|
|
646
|
-
key,
|
|
647
|
-
bucket,
|
|
648
|
-
uploadId
|
|
649
|
-
});
|
|
650
|
-
const listed = await listAllParts(config.client, { bucket, key, uploadId });
|
|
651
|
-
const parts = listed.map((p) => ({
|
|
652
|
-
partNumber: p.PartNumber ?? 0,
|
|
653
|
-
size: p.Size ?? 0,
|
|
654
|
-
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
655
|
-
}));
|
|
656
|
-
await runLifecycleHook(config.multipart?.onList, {
|
|
657
|
-
request,
|
|
658
|
-
key,
|
|
659
|
-
bucket,
|
|
1118
|
+
await runHook(route2.upload.multipart.guard, {
|
|
1119
|
+
...stored,
|
|
660
1120
|
uploadId,
|
|
661
|
-
|
|
1121
|
+
action: "list"
|
|
662
1122
|
});
|
|
1123
|
+
const listed = await listAllParts(route2.client, { bucket, key, uploadId });
|
|
1124
|
+
const parts = listed.flatMap((p) => {
|
|
1125
|
+
if (p.PartNumber == null || p.PartNumber < 1) return [];
|
|
1126
|
+
return [
|
|
1127
|
+
{
|
|
1128
|
+
partNumber: p.PartNumber,
|
|
1129
|
+
size: p.Size ?? 0,
|
|
1130
|
+
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
1131
|
+
}
|
|
1132
|
+
];
|
|
1133
|
+
});
|
|
1134
|
+
await runLifecycleHook(
|
|
1135
|
+
route2.upload.multipart.onList,
|
|
1136
|
+
{
|
|
1137
|
+
...stored,
|
|
1138
|
+
uploadId,
|
|
1139
|
+
parts
|
|
1140
|
+
},
|
|
1141
|
+
config
|
|
1142
|
+
);
|
|
663
1143
|
return { parts };
|
|
664
1144
|
}
|
|
665
1145
|
var multipartListParts = createS3Endpoint(
|
|
666
1146
|
S3_API_ROUTES.multipartListParts,
|
|
667
1147
|
{ method: "GET", query: multipartListPartsQuerySchema },
|
|
668
1148
|
async (ctx) => {
|
|
669
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
670
1149
|
return handleListParts(ctx.context.config, ctx.query, ctx.context.request);
|
|
671
1150
|
}
|
|
672
1151
|
);
|
|
673
1152
|
async function handleSignPart(config, input, request) {
|
|
674
|
-
const { key, bucket } = await
|
|
1153
|
+
const { route: route2, key, bucket, stored } = await openStoredTarget(
|
|
1154
|
+
config,
|
|
1155
|
+
input,
|
|
675
1156
|
request,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
});
|
|
1157
|
+
"multipart"
|
|
1158
|
+
);
|
|
679
1159
|
const uploadId = input.uploadId;
|
|
680
1160
|
const partNumber = input.partNumber;
|
|
681
|
-
const expiresIn = normalizeExpiresIn(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1161
|
+
const expiresIn = normalizeExpiresIn(
|
|
1162
|
+
route2.upload.expiresIn,
|
|
1163
|
+
config.maxExpiresIn
|
|
1164
|
+
);
|
|
1165
|
+
const partSize = Math.floor(input.partSize);
|
|
1166
|
+
await runHook(route2.upload.multipart.guard, {
|
|
1167
|
+
...stored,
|
|
687
1168
|
uploadId,
|
|
1169
|
+
action: "part",
|
|
688
1170
|
partNumber,
|
|
689
|
-
partSize
|
|
1171
|
+
partSize
|
|
690
1172
|
});
|
|
691
|
-
const
|
|
692
|
-
|
|
1173
|
+
const maxFileSize = route2.upload.maxFileSize;
|
|
1174
|
+
assertWithinMaxFileSize(maxFileSize, partSize);
|
|
1175
|
+
if (typeof maxFileSize === "number") {
|
|
1176
|
+
const listed = await listAllParts(route2.client, { bucket, key, uploadId });
|
|
1177
|
+
assertWithinMaxFileSize(
|
|
1178
|
+
maxFileSize,
|
|
1179
|
+
listedPartsByteSize(listed, partNumber) + partSize
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const url = await getSignedUrl(
|
|
1183
|
+
route2.client,
|
|
693
1184
|
new UploadPartCommand({
|
|
694
1185
|
Bucket: bucket,
|
|
695
1186
|
Key: key,
|
|
696
1187
|
UploadId: uploadId,
|
|
697
1188
|
PartNumber: partNumber,
|
|
698
|
-
|
|
1189
|
+
ContentLength: partSize
|
|
699
1190
|
}),
|
|
700
1191
|
{
|
|
701
1192
|
expiresIn,
|
|
702
|
-
|
|
1193
|
+
signableHeaders: /* @__PURE__ */ new Set(["content-length"])
|
|
703
1194
|
}
|
|
704
1195
|
);
|
|
705
1196
|
return {
|
|
706
|
-
|
|
1197
|
+
url,
|
|
707
1198
|
partNumber,
|
|
708
1199
|
uploadId,
|
|
709
1200
|
bucket,
|
|
710
1201
|
expiresIn,
|
|
711
|
-
|
|
1202
|
+
partSize
|
|
712
1203
|
};
|
|
713
1204
|
}
|
|
714
1205
|
var multipartPart = createS3Endpoint(
|
|
715
1206
|
S3_API_ROUTES.multipartPart,
|
|
716
1207
|
{ method: "POST", body: multipartSignPartBodySchema },
|
|
717
1208
|
async (ctx) => {
|
|
718
|
-
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
719
1209
|
return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
|
|
720
1210
|
}
|
|
721
1211
|
);
|
|
722
|
-
function
|
|
1212
|
+
function objectUserMetadata(metadata) {
|
|
723
1213
|
if (!metadata) return {};
|
|
724
|
-
const
|
|
1214
|
+
const fields = {};
|
|
725
1215
|
for (const [k, v] of Object.entries(metadata)) {
|
|
726
|
-
|
|
1216
|
+
fields[`x-amz-meta-${k}`] = v;
|
|
727
1217
|
}
|
|
728
|
-
return
|
|
1218
|
+
return fields;
|
|
729
1219
|
}
|
|
730
1220
|
async function handleUpload(config, input, request) {
|
|
731
|
-
const
|
|
1221
|
+
const fileSize = Math.floor(input.fileSize);
|
|
1222
|
+
const fileName = input.fileName;
|
|
1223
|
+
const { route: route2, key, bucket, metadata, acl, stored } = await openUploadTarget(
|
|
1224
|
+
config,
|
|
1225
|
+
{
|
|
1226
|
+
route: input.route,
|
|
1227
|
+
fileName,
|
|
1228
|
+
fileSize,
|
|
1229
|
+
contentType: input.contentType,
|
|
1230
|
+
metadata: input.metadata
|
|
1231
|
+
},
|
|
732
1232
|
request,
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
739
|
-
const acl = resolveRequestAcl(config.upload, input.acl);
|
|
740
|
-
const contentType = input.contentType ?? "application/octet-stream";
|
|
741
|
-
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
|
|
742
|
-
if (config.upload?.requireFileSize && fileSize === null) {
|
|
743
|
-
throw errors.fileSizeRequiredUpload();
|
|
1233
|
+
"upload"
|
|
1234
|
+
);
|
|
1235
|
+
const method = route2.upload.method ?? "POST";
|
|
1236
|
+
if (route2.upload.checksum && !input.checksum) {
|
|
1237
|
+
throw errors.validationError("Checksum is required for this route");
|
|
744
1238
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1239
|
+
if (method === "POST" && fileSize > S3_MAX_POST_OBJECT_BYTES) {
|
|
1240
|
+
throw errors.payloadTooLarge(
|
|
1241
|
+
`POST uploads cannot exceed ${S3_MAX_POST_OBJECT_BYTES} bytes. Enable upload.multipart or set upload.method to "PUT".`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
const expiresIn = normalizeExpiresIn(
|
|
1245
|
+
route2.upload.expiresIn,
|
|
1246
|
+
config.maxExpiresIn
|
|
1247
|
+
);
|
|
1248
|
+
const contentType = input.contentType ?? "application/octet-stream";
|
|
1249
|
+
const hookCtx = {
|
|
1250
|
+
...stored,
|
|
1251
|
+
file: {
|
|
1252
|
+
name: fileName,
|
|
1253
|
+
size: fileSize,
|
|
1254
|
+
type: input.contentType
|
|
1255
|
+
},
|
|
1256
|
+
metadata,
|
|
1257
|
+
clientMetadata: input.metadata,
|
|
752
1258
|
acl,
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
1259
|
+
replace: route2.upload.replace
|
|
1260
|
+
};
|
|
1261
|
+
await runHook(route2.upload.guard, hookCtx);
|
|
756
1262
|
if (method === "PUT") {
|
|
757
1263
|
const putHeaders = {
|
|
758
1264
|
"Content-Type": contentType,
|
|
759
|
-
...
|
|
1265
|
+
...objectUserMetadata(metadata),
|
|
1266
|
+
"Content-Disposition": buildContentDisposition(fileName),
|
|
1267
|
+
...input.checksum ? { "x-amz-checksum-sha256": input.checksum } : {}
|
|
760
1268
|
};
|
|
761
|
-
if (input.fileName) {
|
|
762
|
-
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
763
|
-
input.fileName
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
1269
|
const url2 = await getSignedUrl(
|
|
767
|
-
|
|
1270
|
+
route2.client,
|
|
768
1271
|
new PutObjectCommand({
|
|
769
1272
|
Bucket: bucket,
|
|
770
1273
|
Key: key,
|
|
771
1274
|
ContentType: contentType,
|
|
772
1275
|
ACL: acl,
|
|
773
|
-
Metadata:
|
|
774
|
-
|
|
775
|
-
|
|
1276
|
+
Metadata: metadata,
|
|
1277
|
+
ContentDisposition: buildContentDisposition(fileName),
|
|
1278
|
+
ContentLength: fileSize,
|
|
1279
|
+
...input.checksum ? { ChecksumSHA256: input.checksum, ChecksumAlgorithm: "SHA256" } : {}
|
|
776
1280
|
}),
|
|
777
1281
|
{
|
|
778
1282
|
expiresIn,
|
|
779
|
-
|
|
1283
|
+
signableHeaders: /* @__PURE__ */ new Set([
|
|
1284
|
+
"content-length",
|
|
1285
|
+
...input.checksum ? ["x-amz-checksum-sha256"] : []
|
|
1286
|
+
])
|
|
780
1287
|
}
|
|
781
1288
|
);
|
|
782
|
-
await runLifecycleHook(
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
url: url2,
|
|
792
|
-
expiresIn
|
|
793
|
-
});
|
|
1289
|
+
await runLifecycleHook(
|
|
1290
|
+
route2.upload.onPresigned,
|
|
1291
|
+
{
|
|
1292
|
+
...hookCtx,
|
|
1293
|
+
url: url2,
|
|
1294
|
+
expiresIn
|
|
1295
|
+
},
|
|
1296
|
+
config
|
|
1297
|
+
);
|
|
794
1298
|
return {
|
|
795
1299
|
bucket,
|
|
796
1300
|
key,
|
|
@@ -800,39 +1304,32 @@ async function handleUpload(config, input, request) {
|
|
|
800
1304
|
method: "PUT"
|
|
801
1305
|
};
|
|
802
1306
|
}
|
|
803
|
-
const fields = {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
const rangeMin = fileSize ?? 1;
|
|
813
|
-
const rangeMax = fileSize ?? void 0;
|
|
1307
|
+
const fields = {
|
|
1308
|
+
acl,
|
|
1309
|
+
"Content-Type": contentType,
|
|
1310
|
+
"Content-Disposition": buildContentDisposition(fileName),
|
|
1311
|
+
...objectUserMetadata(metadata),
|
|
1312
|
+
...input.checksum ? { "x-amz-checksum-sha256": input.checksum } : {}
|
|
1313
|
+
};
|
|
814
1314
|
const { url, fields: signedFields } = await createPresignedPost(
|
|
815
|
-
|
|
1315
|
+
route2.client,
|
|
816
1316
|
{
|
|
817
1317
|
Bucket: bucket,
|
|
818
1318
|
Key: key,
|
|
819
|
-
Conditions:
|
|
1319
|
+
Conditions: [["content-length-range", fileSize, fileSize]],
|
|
820
1320
|
Fields: fields,
|
|
821
1321
|
Expires: expiresIn
|
|
822
1322
|
}
|
|
823
1323
|
);
|
|
824
|
-
await runLifecycleHook(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
url,
|
|
834
|
-
expiresIn
|
|
835
|
-
});
|
|
1324
|
+
await runLifecycleHook(
|
|
1325
|
+
route2.upload.onPresigned,
|
|
1326
|
+
{
|
|
1327
|
+
...hookCtx,
|
|
1328
|
+
url,
|
|
1329
|
+
expiresIn
|
|
1330
|
+
},
|
|
1331
|
+
config
|
|
1332
|
+
);
|
|
836
1333
|
return {
|
|
837
1334
|
bucket,
|
|
838
1335
|
key,
|
|
@@ -846,7 +1343,6 @@ var upload = createS3Endpoint(
|
|
|
846
1343
|
S3_API_ROUTES.upload,
|
|
847
1344
|
{ method: "POST", body: uploadBodySchema },
|
|
848
1345
|
async (ctx) => {
|
|
849
|
-
assertFeatureEnabled(ctx.context.config, "upload");
|
|
850
1346
|
return handleUpload(ctx.context.config, ctx.body, ctx.context.request);
|
|
851
1347
|
}
|
|
852
1348
|
);
|
|
@@ -856,7 +1352,10 @@ var coreEndpoints = {
|
|
|
856
1352
|
upload,
|
|
857
1353
|
confirm,
|
|
858
1354
|
download,
|
|
1355
|
+
catalog,
|
|
1356
|
+
file,
|
|
859
1357
|
delete: deleteObject,
|
|
1358
|
+
deleteBatch,
|
|
860
1359
|
multipartInit,
|
|
861
1360
|
multipartPart,
|
|
862
1361
|
multipartListParts,
|
|
@@ -880,19 +1379,24 @@ function chainHooks(...hooks) {
|
|
|
880
1379
|
var FEATURE_HOOK_KEYS = {
|
|
881
1380
|
upload: ["guard", "onPresigned", "confirmGuard", "onConfirmed"],
|
|
882
1381
|
download: ["guard", "onPresigned"],
|
|
883
|
-
delete: ["guard", "onDeleted"]
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
]
|
|
1382
|
+
delete: ["guard", "onDeleted"]
|
|
1383
|
+
};
|
|
1384
|
+
var MULTIPART_HOOK_KEYS = [
|
|
1385
|
+
"onInit",
|
|
1386
|
+
"guard",
|
|
1387
|
+
"onAbort",
|
|
1388
|
+
"onList"
|
|
1389
|
+
];
|
|
1390
|
+
var LIFECYCLE_FEATURE_HOOK_KEYS = {
|
|
1391
|
+
upload: ["onPresigned", "onConfirmed"],
|
|
1392
|
+
download: ["onPresigned"],
|
|
1393
|
+
delete: ["onDeleted"]
|
|
895
1394
|
};
|
|
1395
|
+
var LIFECYCLE_MULTIPART_HOOK_KEYS = [
|
|
1396
|
+
"onInit",
|
|
1397
|
+
"onAbort",
|
|
1398
|
+
"onList"
|
|
1399
|
+
];
|
|
896
1400
|
|
|
897
1401
|
// src/plugin/types.ts
|
|
898
1402
|
var RESERVED_PLUGIN_IDS = [
|
|
@@ -901,35 +1405,74 @@ var RESERVED_PLUGIN_IDS = [
|
|
|
901
1405
|
"context",
|
|
902
1406
|
"getPlugin",
|
|
903
1407
|
"$ERROR_CODES",
|
|
904
|
-
"$Infer"
|
|
1408
|
+
"$Infer",
|
|
1409
|
+
"upload",
|
|
1410
|
+
"confirm",
|
|
1411
|
+
"download",
|
|
1412
|
+
"catalog",
|
|
1413
|
+
"file",
|
|
1414
|
+
"delete",
|
|
1415
|
+
"deleteBatch",
|
|
1416
|
+
"put",
|
|
1417
|
+
"multipart"
|
|
905
1418
|
];
|
|
906
1419
|
|
|
907
1420
|
// src/plugin/apply-plugins.ts
|
|
908
|
-
function mergeHookField(pluginHooks, userHook) {
|
|
909
|
-
const present = [...pluginHooks, userHook].filter(
|
|
910
|
-
(h) => h != null
|
|
911
|
-
);
|
|
1421
|
+
function mergeHookField(pluginHooks, userHook, order = "plugins-first") {
|
|
1422
|
+
const present = (order === "user-first" ? [userHook, ...pluginHooks] : [...pluginHooks, userHook]).filter((h) => h != null);
|
|
912
1423
|
if (present.length === 0) return void 0;
|
|
913
1424
|
if (present.length === 1) return present[0];
|
|
914
1425
|
return chainHooks(...present);
|
|
915
1426
|
}
|
|
916
|
-
function
|
|
917
|
-
|
|
918
|
-
return userFeature;
|
|
919
|
-
}
|
|
920
|
-
const merged = { ...userFeature ?? {} };
|
|
1427
|
+
function mergeHooks(keys, plugins, userFeature, pick, lifecycleKeys = /* @__PURE__ */ new Set()) {
|
|
1428
|
+
const merged = { ...userFeature };
|
|
921
1429
|
for (const key of keys) {
|
|
922
|
-
const
|
|
923
|
-
(
|
|
1430
|
+
const next = mergeHookField(
|
|
1431
|
+
plugins.map((plugin) => pick(plugin)?.[key]),
|
|
1432
|
+
userFeature[key],
|
|
1433
|
+
lifecycleKeys.has(key) ? "user-first" : "plugins-first"
|
|
924
1434
|
);
|
|
925
|
-
|
|
926
|
-
const next = mergeHookField(fromPlugins, userHook);
|
|
927
|
-
if (next !== void 0) {
|
|
928
|
-
merged[key] = next;
|
|
929
|
-
}
|
|
1435
|
+
if (next !== void 0) merged[key] = next;
|
|
930
1436
|
}
|
|
931
1437
|
return merged;
|
|
932
1438
|
}
|
|
1439
|
+
function applyHooksToRoute(route2, plugins) {
|
|
1440
|
+
const active = plugins.filter((p) => !route2.skippedPluginIds.has(p.id));
|
|
1441
|
+
const upload2 = isEnabled(route2.upload) ? {
|
|
1442
|
+
...mergeHooks(
|
|
1443
|
+
FEATURE_HOOK_KEYS.upload,
|
|
1444
|
+
active,
|
|
1445
|
+
route2.upload,
|
|
1446
|
+
(p) => p.hooks?.upload,
|
|
1447
|
+
new Set(LIFECYCLE_FEATURE_HOOK_KEYS.upload)
|
|
1448
|
+
),
|
|
1449
|
+
multipart: isEnabled(route2.upload.multipart) ? mergeHooks(
|
|
1450
|
+
MULTIPART_HOOK_KEYS,
|
|
1451
|
+
active,
|
|
1452
|
+
route2.upload.multipart,
|
|
1453
|
+
(p) => p.hooks?.upload?.multipart,
|
|
1454
|
+
new Set(LIFECYCLE_MULTIPART_HOOK_KEYS)
|
|
1455
|
+
) : route2.upload.multipart
|
|
1456
|
+
} : route2.upload;
|
|
1457
|
+
return {
|
|
1458
|
+
...route2,
|
|
1459
|
+
upload: upload2,
|
|
1460
|
+
download: isEnabled(route2.download) ? mergeHooks(
|
|
1461
|
+
FEATURE_HOOK_KEYS.download,
|
|
1462
|
+
active,
|
|
1463
|
+
route2.download,
|
|
1464
|
+
(p) => p.hooks?.download,
|
|
1465
|
+
new Set(LIFECYCLE_FEATURE_HOOK_KEYS.download)
|
|
1466
|
+
) : route2.download,
|
|
1467
|
+
delete: isEnabled(route2.delete) ? mergeHooks(
|
|
1468
|
+
FEATURE_HOOK_KEYS.delete,
|
|
1469
|
+
active,
|
|
1470
|
+
route2.delete,
|
|
1471
|
+
(p) => p.hooks?.delete,
|
|
1472
|
+
new Set(LIFECYCLE_FEATURE_HOOK_KEYS.delete)
|
|
1473
|
+
) : route2.delete
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
933
1476
|
function applyPlugins(config) {
|
|
934
1477
|
const plugins = config.plugins ?? [];
|
|
935
1478
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1018,37 +1561,25 @@ function applyPlugins(config) {
|
|
|
1018
1561
|
}
|
|
1019
1562
|
}
|
|
1020
1563
|
const { plugins: _omit, ...rest } = config;
|
|
1021
|
-
|
|
1564
|
+
const normalized = normalizeRoutes(rest);
|
|
1022
1565
|
for (const plugin of plugins) {
|
|
1023
1566
|
plugin.init?.({ config: rest, getPlugin });
|
|
1024
1567
|
}
|
|
1025
|
-
const
|
|
1026
|
-
const {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
delete: _delete,
|
|
1030
|
-
multipart: _multipart,
|
|
1031
|
-
...base
|
|
1032
|
-
} = rest;
|
|
1568
|
+
const routes = {};
|
|
1569
|
+
for (const [name, route2] of Object.entries(normalized)) {
|
|
1570
|
+
routes[name] = applyHooksToRoute(route2, plugins);
|
|
1571
|
+
}
|
|
1033
1572
|
const guard = mergeHookField(
|
|
1034
1573
|
plugins.map((p) => p.hooks?.guard),
|
|
1035
1574
|
rest.guard
|
|
1036
1575
|
);
|
|
1037
1576
|
const merged = {
|
|
1038
|
-
...
|
|
1039
|
-
|
|
1577
|
+
...rest,
|
|
1578
|
+
routes,
|
|
1040
1579
|
...guard !== void 0 ? { guard } : {}
|
|
1041
1580
|
};
|
|
1042
|
-
for (const feature of Object.keys(FEATURE_HOOK_KEYS)) {
|
|
1043
|
-
merged[feature] = mergeFeatureHooks(
|
|
1044
|
-
feature,
|
|
1045
|
-
FEATURE_HOOK_KEYS[feature],
|
|
1046
|
-
plugins,
|
|
1047
|
-
merged[feature]
|
|
1048
|
-
);
|
|
1049
|
-
}
|
|
1050
1581
|
return {
|
|
1051
|
-
config:
|
|
1582
|
+
config: merged,
|
|
1052
1583
|
context,
|
|
1053
1584
|
getPlugin,
|
|
1054
1585
|
endpoints
|
|
@@ -1089,15 +1620,18 @@ var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
|
1089
1620
|
"ETIMEDOUT",
|
|
1090
1621
|
"ENOTFOUND"
|
|
1091
1622
|
]);
|
|
1092
|
-
function onS3RouterError(error) {
|
|
1623
|
+
function onS3RouterError(error, config) {
|
|
1093
1624
|
if (isAPIError(error)) return;
|
|
1094
1625
|
const code = error?.code;
|
|
1626
|
+
const logger = resolveLogger(config?.logger);
|
|
1095
1627
|
if (typeof code === "string" && NETWORK_CODES.has(code)) {
|
|
1096
1628
|
const networkErr = errors.s3NetworkError(code);
|
|
1097
|
-
|
|
1629
|
+
logger.error?.(networkErr.message, error);
|
|
1630
|
+
config?.onError?.(error, {});
|
|
1098
1631
|
throw networkErr;
|
|
1099
1632
|
}
|
|
1100
|
-
|
|
1633
|
+
logger.error?.(error instanceof Error ? error.message : String(error), error);
|
|
1634
|
+
config?.onError?.(error, {});
|
|
1101
1635
|
throw errors.internalError();
|
|
1102
1636
|
}
|
|
1103
1637
|
function withUnmatchedRouteJson(handler) {
|
|
@@ -1117,7 +1651,7 @@ function createS3Router(endpoints, env) {
|
|
|
1117
1651
|
basePath,
|
|
1118
1652
|
routerContext: { config: env.config },
|
|
1119
1653
|
openapi: { disabled: true },
|
|
1120
|
-
onError: onS3RouterError
|
|
1654
|
+
onError: (error) => onS3RouterError(error, env.config)
|
|
1121
1655
|
});
|
|
1122
1656
|
return {
|
|
1123
1657
|
endpoints: bindEndpoints(router.endpoints, { config: env.config }),
|
|
@@ -1138,6 +1672,7 @@ function dimahS3(config) {
|
|
|
1138
1672
|
const bound = router.endpoints;
|
|
1139
1673
|
const api = {
|
|
1140
1674
|
...bound,
|
|
1675
|
+
deleteMany: bound.deleteBatch,
|
|
1141
1676
|
multipart: {
|
|
1142
1677
|
init: bound.multipartInit,
|
|
1143
1678
|
signPart: bound.multipartPart,
|
|
@@ -1153,10 +1688,15 @@ function dimahS3(config) {
|
|
|
1153
1688
|
getPlugin,
|
|
1154
1689
|
$ERROR_CODES: S3_ERROR_CODES,
|
|
1155
1690
|
$Infer: {},
|
|
1691
|
+
put: (input) => putObject(resolved, input),
|
|
1156
1692
|
...context
|
|
1157
1693
|
};
|
|
1158
1694
|
}
|
|
1159
1695
|
|
|
1160
|
-
|
|
1696
|
+
// src/types/config.ts
|
|
1697
|
+
var ROUTE_FEATURES = ["upload", "download", "delete"];
|
|
1698
|
+
var ROUTE_OPERATIONS = [...ROUTE_FEATURES, "multipart"];
|
|
1699
|
+
|
|
1700
|
+
export { ROUTE_FEATURES, ROUTE_OPERATIONS, chainHooks, createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors, isFeatureOn, putObject, route };
|
|
1161
1701
|
//# sourceMappingURL=index.js.map
|
|
1162
1702
|
//# sourceMappingURL=index.js.map
|