@dimah-s3/server 0.7.3 → 0.8.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 +8 -9
- package/dist/adapters/express.js +36 -49
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.js +36 -49
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/next.d.ts +3 -1
- package/dist/adapters/next.d.ts.map +1 -1
- package/dist/adapters/next.js +2 -0
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/node.d.ts +1 -1
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +36 -49
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/api/assert-feature-enabled.d.ts +5 -0
- package/dist/api/assert-feature-enabled.d.ts.map +1 -0
- package/dist/api/bind-endpoints.d.ts +8 -0
- package/dist/api/bind-endpoints.d.ts.map +1 -0
- package/dist/api/context.d.ts +10 -0
- package/dist/api/context.d.ts.map +1 -0
- package/dist/api/create-s3-endpoint.d.ts +141 -0
- package/dist/api/create-s3-endpoint.d.ts.map +1 -0
- package/dist/api/create-s3-middleware.d.ts +23 -0
- package/dist/api/create-s3-middleware.d.ts.map +1 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +758 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/routes/confirm.d.ts +14 -0
- package/dist/api/routes/confirm.d.ts.map +1 -0
- package/dist/api/routes/delete.d.ts +16 -0
- package/dist/api/routes/delete.d.ts.map +1 -0
- package/dist/api/routes/download.d.ts +19 -0
- package/dist/api/routes/download.d.ts.map +1 -0
- package/dist/api/routes/index.d.ts +170 -0
- package/dist/api/routes/index.d.ts.map +1 -0
- package/dist/api/routes/multipart/abort.d.ts +20 -0
- package/dist/api/routes/multipart/abort.d.ts.map +1 -0
- package/dist/api/routes/multipart/complete.d.ts +22 -0
- package/dist/api/routes/multipart/complete.d.ts.map +1 -0
- package/dist/api/routes/multipart/init.d.ts +26 -0
- package/dist/api/routes/multipart/init.d.ts.map +1 -0
- package/dist/api/routes/multipart/list-parts.d.ts +17 -0
- package/dist/api/routes/multipart/list-parts.d.ts.map +1 -0
- package/dist/api/routes/multipart/part.d.ts +22 -0
- package/dist/api/routes/multipart/part.d.ts.map +1 -0
- package/dist/api/routes/upload.d.ts +26 -0
- package/dist/api/routes/upload.d.ts.map +1 -0
- package/dist/dimah-s3.d.ts +41 -12
- package/dist/dimah-s3.d.ts.map +1 -1
- package/dist/errors.d.ts +8 -22
- package/dist/errors.d.ts.map +1 -1
- package/dist/helpers/resolve-target.d.ts +51 -0
- package/dist/helpers/resolve-target.d.ts.map +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +521 -483
- package/dist/index.js.map +1 -1
- package/dist/internal-helpers.d.ts +4 -8
- package/dist/internal-helpers.d.ts.map +1 -1
- package/dist/plugin/apply-plugins.d.ts.map +1 -1
- package/dist/plugin/chain-hooks.d.ts +1 -1
- package/dist/plugin/define-plugin.d.ts +4 -13
- package/dist/plugin/define-plugin.d.ts.map +1 -1
- package/dist/plugin/hook-registry.d.ts +2 -2
- 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 +21 -41
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugins.d.ts +7 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +984 -0
- package/dist/plugins.js.map +1 -0
- package/dist/router.d.ts +12 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/types/config.d.ts +61 -22
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hook-contexts.d.ts +11 -6
- package/dist/types/hook-contexts.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +16 -6
- package/dist/adapters/adapters.test.d.ts +0 -2
- package/dist/adapters/adapters.test.d.ts.map +0 -1
- package/dist/api.d.ts +0 -5
- package/dist/api.d.ts.map +0 -1
- package/dist/handler.d.ts +0 -5
- package/dist/handler.d.ts.map +0 -1
- package/dist/helpers/is-aws-not-found.test.d.ts +0 -2
- package/dist/helpers/is-aws-not-found.test.d.ts.map +0 -1
- package/dist/internal-helpers.test.d.ts +0 -2
- package/dist/internal-helpers.test.d.ts.map +0 -1
- package/dist/plugin/apply-plugins.test.d.ts +0 -2
- package/dist/plugin/apply-plugins.test.d.ts.map +0 -1
- package/dist/plugin/create-endpoint.d.ts +0 -20
- package/dist/plugin/create-endpoint.d.ts.map +0 -1
- package/dist/procedures/confirm.d.ts +0 -8
- package/dist/procedures/confirm.d.ts.map +0 -1
- package/dist/procedures/delete.d.ts +0 -8
- package/dist/procedures/delete.d.ts.map +0 -1
- package/dist/procedures/download.d.ts +0 -10
- package/dist/procedures/download.d.ts.map +0 -1
- package/dist/procedures/multipart/abort.d.ts +0 -13
- package/dist/procedures/multipart/abort.d.ts.map +0 -1
- package/dist/procedures/multipart/complete.d.ts +0 -12
- package/dist/procedures/multipart/complete.d.ts.map +0 -1
- package/dist/procedures/multipart/init.d.ts +0 -13
- package/dist/procedures/multipart/init.d.ts.map +0 -1
- package/dist/procedures/multipart/list-parts.d.ts +0 -9
- package/dist/procedures/multipart/list-parts.d.ts.map +0 -1
- package/dist/procedures/multipart/part.d.ts +0 -12
- package/dist/procedures/multipart/part.d.ts.map +0 -1
- package/dist/procedures/registry.d.ts +0 -78
- package/dist/procedures/registry.d.ts.map +0 -1
- package/dist/procedures/upload.d.ts +0 -14
- package/dist/procedures/upload.d.ts.map +0 -1
|
@@ -0,0 +1,758 @@
|
|
|
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, parseFileName, buildContentDisposition } from '@dimah-s3/core';
|
|
3
|
+
import { HeadObjectCommand, DeleteObjectCommand, GetObjectCommand, AbortMultipartUploadCommand, ListPartsCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand } from '@aws-sdk/client-s3';
|
|
4
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
|
+
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
6
|
+
|
|
7
|
+
// src/api/create-s3-endpoint.ts
|
|
8
|
+
var errors = {
|
|
9
|
+
notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
|
|
10
|
+
unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
|
|
11
|
+
forbidden: () => DimahS3Error.from("FORBIDDEN", S3_ERROR_CODES.FORBIDDEN),
|
|
12
|
+
conflict: () => DimahS3Error.from("CONFLICT", S3_ERROR_CODES.CONFLICT),
|
|
13
|
+
internalError: () => DimahS3Error.from("INTERNAL_SERVER_ERROR", S3_ERROR_CODES.INTERNAL_ERROR),
|
|
14
|
+
objectNotFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.OBJECT_NOT_FOUND),
|
|
15
|
+
featureDisabled: (feature) => DimahS3Error.from("NOT_FOUND", {
|
|
16
|
+
code: S3_ERROR_CODES.FEATURE_DISABLED.code,
|
|
17
|
+
message: S3_ERROR_CODES.FEATURE_DISABLED.message.replaceAll(
|
|
18
|
+
"{feature}",
|
|
19
|
+
feature
|
|
20
|
+
),
|
|
21
|
+
params: { feature }
|
|
22
|
+
}),
|
|
23
|
+
invalidKey: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.INVALID_KEY),
|
|
24
|
+
invalidBucket: (bucket) => DimahS3Error.from("FORBIDDEN", {
|
|
25
|
+
code: S3_ERROR_CODES.INVALID_BUCKET.code,
|
|
26
|
+
message: S3_ERROR_CODES.INVALID_BUCKET.message,
|
|
27
|
+
params: { bucket }
|
|
28
|
+
}),
|
|
29
|
+
s3NetworkError: (code) => DimahS3Error.from("BAD_GATEWAY", {
|
|
30
|
+
code: S3_ERROR_CODES.S3_NETWORK_ERROR.code,
|
|
31
|
+
message: S3_ERROR_CODES.S3_NETWORK_ERROR.message.replaceAll(
|
|
32
|
+
"{code}",
|
|
33
|
+
code
|
|
34
|
+
),
|
|
35
|
+
params: { code }
|
|
36
|
+
}),
|
|
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
|
+
validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
|
|
43
|
+
code: S3_ERROR_CODES.VALIDATION_ERROR.code,
|
|
44
|
+
message
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
async function runHook(hook, context) {
|
|
48
|
+
if (!hook) return;
|
|
49
|
+
try {
|
|
50
|
+
await hook(context);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (isAPIError(err)) throw err;
|
|
53
|
+
if (err instanceof Error && err.message.trim()) {
|
|
54
|
+
throw new DimahS3Error("FORBIDDEN", {
|
|
55
|
+
message: err.message,
|
|
56
|
+
cause: err
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
throw errors.forbidden();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function runLifecycleHook(hook, context) {
|
|
63
|
+
if (!hook) return;
|
|
64
|
+
try {
|
|
65
|
+
await hook(context);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (isAPIError(err)) throw err;
|
|
68
|
+
console.error("[S3 API] lifecycle hook failed", err);
|
|
69
|
+
throw errors.internalError();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function normalizeExpiresIn(value) {
|
|
73
|
+
const n = Number(value);
|
|
74
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
|
|
75
|
+
}
|
|
76
|
+
function requestFromHeaders(headers) {
|
|
77
|
+
return new Request("http://dimah-s3.local", { headers });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/api/create-s3-middleware.ts
|
|
81
|
+
var s3ContextMiddleware = createMiddleware(async (ctx) => {
|
|
82
|
+
const injected = ctx.context;
|
|
83
|
+
if (!injected?.config) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"createS3Endpoint requires dimahS3 router context. Call endpoints through dimahS3().api or the HTTP handler."
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const request = ctx.request ?? injected.request ?? requestFromHeaders(ctx.headers);
|
|
89
|
+
await runHook(injected.config.guard, { request });
|
|
90
|
+
return {
|
|
91
|
+
config: injected.config,
|
|
92
|
+
request
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
var createS3Middleware = createMiddleware.create({
|
|
96
|
+
use: [s3ContextMiddleware]
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// src/api/create-s3-endpoint.ts
|
|
100
|
+
var createEndpointWithContext = createEndpoint.create({
|
|
101
|
+
use: [s3ContextMiddleware]
|
|
102
|
+
});
|
|
103
|
+
function withS3Validation(options) {
|
|
104
|
+
return {
|
|
105
|
+
...options,
|
|
106
|
+
onValidationError: options.onValidationError ?? (({ message }) => {
|
|
107
|
+
throw errors.validationError(message);
|
|
108
|
+
})
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
var createS3Endpoint = ((pathOrOptions, optionsOrHandler, maybeHandler) => {
|
|
112
|
+
if (typeof pathOrOptions === "string") {
|
|
113
|
+
return createEndpointWithContext(
|
|
114
|
+
pathOrOptions,
|
|
115
|
+
withS3Validation(optionsOrHandler),
|
|
116
|
+
maybeHandler
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return createEndpointWithContext(
|
|
120
|
+
withS3Validation(pathOrOptions),
|
|
121
|
+
optionsOrHandler
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// src/helpers/is-aws-not-found.ts
|
|
126
|
+
function isAwsNotFound(err) {
|
|
127
|
+
const e = err;
|
|
128
|
+
return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
|
|
129
|
+
}
|
|
130
|
+
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
131
|
+
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
134
|
+
try {
|
|
135
|
+
const result = await s3.send(
|
|
136
|
+
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
137
|
+
{ abortSignal: controller.signal }
|
|
138
|
+
);
|
|
139
|
+
const isPublic = result.Grants?.some(
|
|
140
|
+
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
141
|
+
);
|
|
142
|
+
return isPublic ? "public-read" : "private";
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
} finally {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/helpers/resolve-target.ts
|
|
151
|
+
function assertSafeObjectKey(key) {
|
|
152
|
+
const trimmed = key.replace(/^\/+/u, "").replace(/\/+$/u, "");
|
|
153
|
+
if (!trimmed) {
|
|
154
|
+
throw errors.invalidKey();
|
|
155
|
+
}
|
|
156
|
+
if (trimmed.includes("\\") || trimmed.includes("\0")) {
|
|
157
|
+
throw errors.invalidKey();
|
|
158
|
+
}
|
|
159
|
+
const parts = trimmed.split("/");
|
|
160
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
161
|
+
throw errors.invalidKey();
|
|
162
|
+
}
|
|
163
|
+
return parts.join("/");
|
|
164
|
+
}
|
|
165
|
+
function applyPrefix(prefix, key) {
|
|
166
|
+
const p = assertSafeObjectKey(prefix);
|
|
167
|
+
const k = assertSafeObjectKey(key);
|
|
168
|
+
if (k === p || k.startsWith(`${p}/`)) return k;
|
|
169
|
+
return `${p}/${k}`;
|
|
170
|
+
}
|
|
171
|
+
async function resolveObjectKey(policy, context) {
|
|
172
|
+
if (policy?.resolveKey) {
|
|
173
|
+
return assertSafeObjectKey(await policy.resolveKey(context));
|
|
174
|
+
}
|
|
175
|
+
const prefix = typeof policy?.prefix === "function" ? await policy.prefix(context) : policy?.prefix;
|
|
176
|
+
if (prefix) {
|
|
177
|
+
return applyPrefix(prefix, context.proposedKey);
|
|
178
|
+
}
|
|
179
|
+
return assertSafeObjectKey(context.proposedKey);
|
|
180
|
+
}
|
|
181
|
+
function resolveBucket(config, requested) {
|
|
182
|
+
if (!requested) return config.bucket;
|
|
183
|
+
if (config.allowClientBucket) return requested;
|
|
184
|
+
if (config.buckets?.length) {
|
|
185
|
+
if (config.buckets.includes(requested)) return requested;
|
|
186
|
+
throw errors.invalidBucket(requested);
|
|
187
|
+
}
|
|
188
|
+
return config.bucket;
|
|
189
|
+
}
|
|
190
|
+
async function resolveRequestTarget(config, policy, input) {
|
|
191
|
+
const bucket = resolveBucket(config, input.bucket);
|
|
192
|
+
const key = await resolveObjectKey(policy, {
|
|
193
|
+
request: input.request,
|
|
194
|
+
proposedKey: input.key,
|
|
195
|
+
fileName: input.fileName,
|
|
196
|
+
contentType: input.contentType,
|
|
197
|
+
bucket
|
|
198
|
+
});
|
|
199
|
+
return { key, bucket };
|
|
200
|
+
}
|
|
201
|
+
function isFeatureEnabled(config, feature) {
|
|
202
|
+
return config[feature]?.enabled === true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/api/assert-feature-enabled.ts
|
|
206
|
+
function assertFeatureEnabled(config, feature) {
|
|
207
|
+
if (!isFeatureEnabled(config, feature)) {
|
|
208
|
+
throw errors.featureDisabled(feature);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/api/routes/confirm.ts
|
|
213
|
+
async function handleConfirm(config, input, request) {
|
|
214
|
+
const { key, bucket } = await resolveRequestTarget(config, config.upload, {
|
|
215
|
+
request,
|
|
216
|
+
key: input.key,
|
|
217
|
+
bucket: input.bucket
|
|
218
|
+
});
|
|
219
|
+
await runHook(config.upload?.confirmGuard, {
|
|
220
|
+
request,
|
|
221
|
+
key,
|
|
222
|
+
bucket
|
|
223
|
+
});
|
|
224
|
+
const head = await config.client.send(
|
|
225
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
226
|
+
);
|
|
227
|
+
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
|
|
228
|
+
const fileName = parseFileName(head.ContentDisposition);
|
|
229
|
+
const context = {
|
|
230
|
+
request,
|
|
231
|
+
key,
|
|
232
|
+
bucket,
|
|
233
|
+
contentType: head.ContentType,
|
|
234
|
+
contentLength: head.ContentLength ?? 0,
|
|
235
|
+
eTag: head.ETag?.replace(/"/g, ""),
|
|
236
|
+
metadata: head.Metadata,
|
|
237
|
+
acl,
|
|
238
|
+
fileName,
|
|
239
|
+
versionId: head.VersionId,
|
|
240
|
+
lastModified: head.LastModified?.toISOString()
|
|
241
|
+
};
|
|
242
|
+
await runLifecycleHook(config.upload?.onConfirmed, context);
|
|
243
|
+
return {
|
|
244
|
+
key,
|
|
245
|
+
bucket,
|
|
246
|
+
contentType: context.contentType,
|
|
247
|
+
contentLength: context.contentLength,
|
|
248
|
+
eTag: context.eTag,
|
|
249
|
+
metadata: context.metadata ?? {},
|
|
250
|
+
acl,
|
|
251
|
+
fileName,
|
|
252
|
+
versionId: context.versionId,
|
|
253
|
+
lastModified: context.lastModified
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
var confirm = createS3Endpoint(
|
|
257
|
+
S3_API_ROUTES.uploadConfirm,
|
|
258
|
+
{ method: "POST", body: confirmBodySchema },
|
|
259
|
+
async (ctx) => {
|
|
260
|
+
assertFeatureEnabled(ctx.context.config, "upload");
|
|
261
|
+
return handleConfirm(ctx.context.config, ctx.body, ctx.context.request);
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
async function handleDelete(config, input, request) {
|
|
265
|
+
const { key, bucket } = await resolveRequestTarget(config, config.delete, {
|
|
266
|
+
request,
|
|
267
|
+
key: input.key,
|
|
268
|
+
bucket: input.bucket
|
|
269
|
+
});
|
|
270
|
+
await runHook(config.delete?.guard, {
|
|
271
|
+
request,
|
|
272
|
+
key,
|
|
273
|
+
bucket
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
await config.client.send(
|
|
277
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
278
|
+
);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (isAwsNotFound(err)) {
|
|
281
|
+
throw errors.objectNotFound();
|
|
282
|
+
}
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
await config.client.send(
|
|
286
|
+
new DeleteObjectCommand({ Bucket: bucket, Key: key })
|
|
287
|
+
);
|
|
288
|
+
await runLifecycleHook(config.delete?.onDeleted, { request, key, bucket });
|
|
289
|
+
return { success: true, bucket, key };
|
|
290
|
+
}
|
|
291
|
+
var deleteObject = createS3Endpoint(
|
|
292
|
+
S3_API_ROUTES.delete,
|
|
293
|
+
{ method: "DELETE", query: deleteQuerySchema },
|
|
294
|
+
async (ctx) => {
|
|
295
|
+
assertFeatureEnabled(ctx.context.config, "delete");
|
|
296
|
+
return handleDelete(ctx.context.config, ctx.query, ctx.context.request);
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
async function handleDownload(config, input, request) {
|
|
300
|
+
const { key, bucket } = await resolveRequestTarget(config, config.download, {
|
|
301
|
+
request,
|
|
302
|
+
key: input.key,
|
|
303
|
+
bucket: input.bucket,
|
|
304
|
+
fileName: input.fileName
|
|
305
|
+
});
|
|
306
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
307
|
+
const fileName = input.fileName;
|
|
308
|
+
await runHook(config.download?.guard, {
|
|
309
|
+
request,
|
|
310
|
+
key,
|
|
311
|
+
bucket,
|
|
312
|
+
fileName
|
|
313
|
+
});
|
|
314
|
+
try {
|
|
315
|
+
await config.client.send(
|
|
316
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
317
|
+
);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (isAwsNotFound(err)) {
|
|
320
|
+
throw errors.objectNotFound();
|
|
321
|
+
}
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
const url = await getSignedUrl(
|
|
325
|
+
config.client,
|
|
326
|
+
new GetObjectCommand({
|
|
327
|
+
Bucket: bucket,
|
|
328
|
+
Key: key,
|
|
329
|
+
ResponseContentDisposition: fileName ? buildContentDisposition(fileName) : "attachment"
|
|
330
|
+
}),
|
|
331
|
+
{ expiresIn }
|
|
332
|
+
);
|
|
333
|
+
await runLifecycleHook(config.download?.onPresigned, {
|
|
334
|
+
request,
|
|
335
|
+
key,
|
|
336
|
+
bucket,
|
|
337
|
+
fileName,
|
|
338
|
+
url,
|
|
339
|
+
expiresIn
|
|
340
|
+
});
|
|
341
|
+
return { bucket, key, url, expiresIn };
|
|
342
|
+
}
|
|
343
|
+
var download = createS3Endpoint(
|
|
344
|
+
S3_API_ROUTES.download,
|
|
345
|
+
{ method: "GET", query: downloadQuerySchema },
|
|
346
|
+
async (ctx) => {
|
|
347
|
+
assertFeatureEnabled(ctx.context.config, "download");
|
|
348
|
+
return handleDownload(ctx.context.config, ctx.query, ctx.context.request);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
async function handleAbort(config, input, request) {
|
|
352
|
+
const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
|
|
353
|
+
request,
|
|
354
|
+
key: input.key,
|
|
355
|
+
bucket: input.bucket
|
|
356
|
+
});
|
|
357
|
+
const uploadId = input.uploadId;
|
|
358
|
+
await runHook(config.multipart?.abortGuard, {
|
|
359
|
+
request,
|
|
360
|
+
key,
|
|
361
|
+
bucket,
|
|
362
|
+
uploadId
|
|
363
|
+
});
|
|
364
|
+
await config.client.send(
|
|
365
|
+
new AbortMultipartUploadCommand({
|
|
366
|
+
Bucket: bucket,
|
|
367
|
+
Key: key,
|
|
368
|
+
UploadId: uploadId
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
await runLifecycleHook(config.multipart?.onAbort, {
|
|
372
|
+
request,
|
|
373
|
+
key,
|
|
374
|
+
bucket,
|
|
375
|
+
uploadId
|
|
376
|
+
});
|
|
377
|
+
return { bucket, key, uploadId, aborted: true };
|
|
378
|
+
}
|
|
379
|
+
var multipartAbort = createS3Endpoint(
|
|
380
|
+
S3_API_ROUTES.multipartAbort,
|
|
381
|
+
{ method: "POST", body: multipartAbortBodySchema },
|
|
382
|
+
async (ctx) => {
|
|
383
|
+
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
384
|
+
return handleAbort(ctx.context.config, ctx.body, ctx.context.request);
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
async function handleComplete(config, input, request) {
|
|
388
|
+
const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
|
|
389
|
+
request,
|
|
390
|
+
key: input.key,
|
|
391
|
+
bucket: input.bucket
|
|
392
|
+
});
|
|
393
|
+
const uploadId = input.uploadId;
|
|
394
|
+
const parts = input.parts.map(({ partNumber }) => partNumber).sort((a, b) => a - b);
|
|
395
|
+
const partRefs = parts.map((partNumber) => ({ partNumber }));
|
|
396
|
+
await runHook(config.multipart?.completeGuard, {
|
|
397
|
+
request,
|
|
398
|
+
key,
|
|
399
|
+
bucket,
|
|
400
|
+
uploadId,
|
|
401
|
+
parts: partRefs
|
|
402
|
+
});
|
|
403
|
+
const listed = await config.client.send(
|
|
404
|
+
new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
|
|
405
|
+
);
|
|
406
|
+
const listedParts = listed.Parts ?? [];
|
|
407
|
+
const completeParts = parts.map((partNumber) => {
|
|
408
|
+
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
409
|
+
return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
|
|
410
|
+
});
|
|
411
|
+
const completeResult = await config.client.send(
|
|
412
|
+
new CompleteMultipartUploadCommand({
|
|
413
|
+
Bucket: bucket,
|
|
414
|
+
Key: key,
|
|
415
|
+
UploadId: uploadId,
|
|
416
|
+
MultipartUpload: { Parts: completeParts }
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
let head = await config.client.send(
|
|
420
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
421
|
+
);
|
|
422
|
+
for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
|
|
423
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
424
|
+
head = await config.client.send(
|
|
425
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const contentLength = head.ContentLength ?? 0;
|
|
429
|
+
const contentType = head.ContentType;
|
|
430
|
+
const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
|
|
431
|
+
const metadata = head.Metadata ?? {};
|
|
432
|
+
const versionId = head.VersionId;
|
|
433
|
+
const lastModified = head.LastModified?.toISOString();
|
|
434
|
+
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
|
|
435
|
+
const fileName = parseFileName(head.ContentDisposition);
|
|
436
|
+
await runLifecycleHook(config.multipart?.onComplete, {
|
|
437
|
+
request,
|
|
438
|
+
key,
|
|
439
|
+
bucket,
|
|
440
|
+
uploadId,
|
|
441
|
+
contentLength,
|
|
442
|
+
contentType,
|
|
443
|
+
eTag,
|
|
444
|
+
metadata,
|
|
445
|
+
acl,
|
|
446
|
+
fileName,
|
|
447
|
+
versionId,
|
|
448
|
+
lastModified
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
bucket,
|
|
452
|
+
key,
|
|
453
|
+
uploadId,
|
|
454
|
+
contentLength,
|
|
455
|
+
contentType,
|
|
456
|
+
eTag,
|
|
457
|
+
metadata,
|
|
458
|
+
acl,
|
|
459
|
+
fileName,
|
|
460
|
+
versionId,
|
|
461
|
+
lastModified
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
var multipartComplete = createS3Endpoint(
|
|
465
|
+
S3_API_ROUTES.multipartComplete,
|
|
466
|
+
{ method: "POST", body: multipartCompleteBodySchema },
|
|
467
|
+
async (ctx) => {
|
|
468
|
+
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
469
|
+
return handleComplete(ctx.context.config, ctx.body, ctx.context.request);
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
async function handleMultipartInit(config, input, request) {
|
|
473
|
+
const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
|
|
474
|
+
request,
|
|
475
|
+
key: input.key,
|
|
476
|
+
bucket: input.bucket,
|
|
477
|
+
fileName: input.fileName,
|
|
478
|
+
contentType: input.contentType
|
|
479
|
+
});
|
|
480
|
+
const acl = input.acl === "public-read" ? "public-read" : "private";
|
|
481
|
+
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
|
|
482
|
+
if (config.multipart?.requireFileSize && fileSize === void 0) {
|
|
483
|
+
throw errors.fileSizeRequiredMultipart();
|
|
484
|
+
}
|
|
485
|
+
await runHook(config.multipart?.initGuard, {
|
|
486
|
+
request,
|
|
487
|
+
key,
|
|
488
|
+
bucket,
|
|
489
|
+
fileSize
|
|
490
|
+
});
|
|
491
|
+
const { UploadId } = await config.client.send(
|
|
492
|
+
new CreateMultipartUploadCommand({
|
|
493
|
+
Bucket: bucket,
|
|
494
|
+
Key: key,
|
|
495
|
+
ContentType: input.contentType,
|
|
496
|
+
ContentDisposition: input.fileName ? buildContentDisposition(input.fileName) : void 0,
|
|
497
|
+
Metadata: input.metadata,
|
|
498
|
+
ACL: acl
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
await runLifecycleHook(config.multipart?.onInit, {
|
|
502
|
+
request,
|
|
503
|
+
key,
|
|
504
|
+
bucket,
|
|
505
|
+
uploadId: UploadId,
|
|
506
|
+
contentType: input.contentType,
|
|
507
|
+
fileSize,
|
|
508
|
+
metadata: input.metadata,
|
|
509
|
+
acl,
|
|
510
|
+
fileName: input.fileName
|
|
511
|
+
});
|
|
512
|
+
return { bucket, key, uploadId: UploadId };
|
|
513
|
+
}
|
|
514
|
+
var multipartInit = createS3Endpoint(
|
|
515
|
+
S3_API_ROUTES.multipartInit,
|
|
516
|
+
{ method: "POST", body: multipartInitBodySchema },
|
|
517
|
+
async (ctx) => {
|
|
518
|
+
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
519
|
+
ctx.setStatus(201);
|
|
520
|
+
return handleMultipartInit(
|
|
521
|
+
ctx.context.config,
|
|
522
|
+
ctx.body,
|
|
523
|
+
ctx.context.request
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
async function handleListParts(config, input, request) {
|
|
528
|
+
const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
|
|
529
|
+
request,
|
|
530
|
+
key: input.key,
|
|
531
|
+
bucket: input.bucket
|
|
532
|
+
});
|
|
533
|
+
const uploadId = input.uploadId;
|
|
534
|
+
await runHook(config.multipart?.listGuard, {
|
|
535
|
+
request,
|
|
536
|
+
key,
|
|
537
|
+
bucket,
|
|
538
|
+
uploadId
|
|
539
|
+
});
|
|
540
|
+
const response = await config.client.send(
|
|
541
|
+
new ListPartsCommand({
|
|
542
|
+
Bucket: bucket,
|
|
543
|
+
Key: key,
|
|
544
|
+
UploadId: uploadId
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
const parts = (response.Parts ?? []).map((p) => ({
|
|
548
|
+
partNumber: p.PartNumber ?? 0,
|
|
549
|
+
size: p.Size ?? 0,
|
|
550
|
+
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
551
|
+
}));
|
|
552
|
+
await runLifecycleHook(config.multipart?.onList, {
|
|
553
|
+
request,
|
|
554
|
+
key,
|
|
555
|
+
bucket,
|
|
556
|
+
uploadId,
|
|
557
|
+
parts
|
|
558
|
+
});
|
|
559
|
+
return { parts };
|
|
560
|
+
}
|
|
561
|
+
var multipartListParts = createS3Endpoint(
|
|
562
|
+
S3_API_ROUTES.multipartListParts,
|
|
563
|
+
{ method: "GET", query: multipartListPartsQuerySchema },
|
|
564
|
+
async (ctx) => {
|
|
565
|
+
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
566
|
+
return handleListParts(ctx.context.config, ctx.query, ctx.context.request);
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
async function handleSignPart(config, input, request) {
|
|
570
|
+
const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
|
|
571
|
+
request,
|
|
572
|
+
key: input.key,
|
|
573
|
+
bucket: input.bucket
|
|
574
|
+
});
|
|
575
|
+
const uploadId = input.uploadId;
|
|
576
|
+
const partNumber = input.partNumber;
|
|
577
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
578
|
+
const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
|
|
579
|
+
await runHook(config.multipart?.partGuard, {
|
|
580
|
+
request,
|
|
581
|
+
key,
|
|
582
|
+
bucket,
|
|
583
|
+
uploadId,
|
|
584
|
+
partNumber,
|
|
585
|
+
partSize: partSize ?? void 0
|
|
586
|
+
});
|
|
587
|
+
const presignedUrl = await getSignedUrl(
|
|
588
|
+
config.client,
|
|
589
|
+
new UploadPartCommand({
|
|
590
|
+
Bucket: bucket,
|
|
591
|
+
Key: key,
|
|
592
|
+
UploadId: uploadId,
|
|
593
|
+
PartNumber: partNumber,
|
|
594
|
+
...partSize !== null ? { ContentLength: partSize } : {}
|
|
595
|
+
}),
|
|
596
|
+
{
|
|
597
|
+
expiresIn,
|
|
598
|
+
...partSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
return {
|
|
602
|
+
presignedUrl,
|
|
603
|
+
partNumber,
|
|
604
|
+
uploadId,
|
|
605
|
+
bucket,
|
|
606
|
+
expiresIn,
|
|
607
|
+
...partSize !== null ? { partSize } : {}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
var multipartPart = createS3Endpoint(
|
|
611
|
+
S3_API_ROUTES.multipartPart,
|
|
612
|
+
{ method: "POST", body: multipartSignPartBodySchema },
|
|
613
|
+
async (ctx) => {
|
|
614
|
+
assertFeatureEnabled(ctx.context.config, "multipart");
|
|
615
|
+
return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
async function handleUpload(config, input, request) {
|
|
619
|
+
const { key, bucket } = await resolveRequestTarget(config, config.upload, {
|
|
620
|
+
request,
|
|
621
|
+
key: input.key,
|
|
622
|
+
bucket: input.bucket,
|
|
623
|
+
fileName: input.fileName,
|
|
624
|
+
contentType: input.contentType
|
|
625
|
+
});
|
|
626
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
627
|
+
const acl = input.acl === "public-read" ? "public-read" : "private";
|
|
628
|
+
const contentType = input.contentType ?? "application/octet-stream";
|
|
629
|
+
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
|
|
630
|
+
await runHook(config.upload?.guard, {
|
|
631
|
+
request,
|
|
632
|
+
key,
|
|
633
|
+
bucket,
|
|
634
|
+
contentType: input.contentType,
|
|
635
|
+
fileSize: fileSize ?? void 0,
|
|
636
|
+
metadata: input.metadata,
|
|
637
|
+
acl,
|
|
638
|
+
fileName: input.fileName
|
|
639
|
+
});
|
|
640
|
+
const method = config.upload?.method ?? "POST";
|
|
641
|
+
if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
|
|
642
|
+
throw errors.fileSizeRequiredUpload();
|
|
643
|
+
}
|
|
644
|
+
if (method === "PUT") {
|
|
645
|
+
const putHeaders = {
|
|
646
|
+
"Content-Type": contentType
|
|
647
|
+
};
|
|
648
|
+
if (input.fileName) {
|
|
649
|
+
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
650
|
+
input.fileName
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const url2 = await getSignedUrl(
|
|
654
|
+
config.client,
|
|
655
|
+
new PutObjectCommand({
|
|
656
|
+
Bucket: bucket,
|
|
657
|
+
Key: key,
|
|
658
|
+
ContentType: contentType,
|
|
659
|
+
ACL: acl,
|
|
660
|
+
...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
|
|
661
|
+
...fileSize !== null ? { ContentLength: fileSize } : {}
|
|
662
|
+
}),
|
|
663
|
+
{
|
|
664
|
+
expiresIn,
|
|
665
|
+
...fileSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
|
|
666
|
+
}
|
|
667
|
+
);
|
|
668
|
+
await runLifecycleHook(config.upload?.onPresigned, {
|
|
669
|
+
request,
|
|
670
|
+
key,
|
|
671
|
+
bucket,
|
|
672
|
+
contentType: input.contentType,
|
|
673
|
+
fileSize: fileSize ?? void 0,
|
|
674
|
+
metadata: input.metadata,
|
|
675
|
+
acl,
|
|
676
|
+
fileName: input.fileName,
|
|
677
|
+
url: url2,
|
|
678
|
+
expiresIn
|
|
679
|
+
});
|
|
680
|
+
return {
|
|
681
|
+
bucket,
|
|
682
|
+
key,
|
|
683
|
+
url: url2,
|
|
684
|
+
headers: putHeaders,
|
|
685
|
+
expiresIn,
|
|
686
|
+
method: "PUT"
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const fields = { acl, "Content-Type": contentType };
|
|
690
|
+
if (input.fileName) {
|
|
691
|
+
fields["Content-Disposition"] = buildContentDisposition(input.fileName);
|
|
692
|
+
}
|
|
693
|
+
if (input.metadata) {
|
|
694
|
+
for (const [k, v] of Object.entries(input.metadata)) {
|
|
695
|
+
fields[`x-amz-meta-${k}`] = v;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const rangeMin = fileSize ?? 1;
|
|
699
|
+
const rangeMax = fileSize ?? void 0;
|
|
700
|
+
const { url, fields: signedFields } = await createPresignedPost(
|
|
701
|
+
config.client,
|
|
702
|
+
{
|
|
703
|
+
Bucket: bucket,
|
|
704
|
+
Key: key,
|
|
705
|
+
Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
|
|
706
|
+
Fields: fields,
|
|
707
|
+
Expires: expiresIn
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
await runLifecycleHook(config.upload?.onPresigned, {
|
|
711
|
+
request,
|
|
712
|
+
key,
|
|
713
|
+
bucket,
|
|
714
|
+
contentType: input.contentType,
|
|
715
|
+
fileSize: fileSize ?? void 0,
|
|
716
|
+
metadata: input.metadata,
|
|
717
|
+
acl,
|
|
718
|
+
fileName: input.fileName,
|
|
719
|
+
url,
|
|
720
|
+
expiresIn
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
bucket,
|
|
724
|
+
key,
|
|
725
|
+
url,
|
|
726
|
+
fields: signedFields,
|
|
727
|
+
expiresIn,
|
|
728
|
+
method: "POST"
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
var upload = createS3Endpoint(
|
|
732
|
+
S3_API_ROUTES.upload,
|
|
733
|
+
{ method: "POST", body: uploadBodySchema },
|
|
734
|
+
async (ctx) => {
|
|
735
|
+
assertFeatureEnabled(ctx.context.config, "upload");
|
|
736
|
+
return handleUpload(ctx.context.config, ctx.body, ctx.context.request);
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
// src/api/routes/index.ts
|
|
741
|
+
var coreEndpoints = {
|
|
742
|
+
upload,
|
|
743
|
+
confirm,
|
|
744
|
+
download,
|
|
745
|
+
delete: deleteObject,
|
|
746
|
+
multipartInit,
|
|
747
|
+
multipartPart,
|
|
748
|
+
multipartListParts,
|
|
749
|
+
multipartComplete,
|
|
750
|
+
multipartAbort
|
|
751
|
+
};
|
|
752
|
+
var CORE_ENDPOINT_NAMES = Object.keys(
|
|
753
|
+
coreEndpoints
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
export { CORE_ENDPOINT_NAMES, coreEndpoints, createS3Endpoint, createS3Middleware, s3ContextMiddleware };
|
|
757
|
+
//# sourceMappingURL=index.js.map
|
|
758
|
+
//# sourceMappingURL=index.js.map
|