@better-s3/server 3.1045.1 → 3.1046.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 +6 -2
- package/dist/adapters/next.js +668 -2
- package/dist/adapters/next.js.map +1 -1
- package/dist/api.d.ts +20 -3
- package/dist/handlers/multipart/list-parts.d.ts +2 -0
- package/dist/helpers/index.d.ts +0 -1
- package/dist/helpers/index.js +23 -1
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/server/index.js +25 -2
- package/dist/helpers/server/index.js.map +1 -1
- package/dist/helpers/server/resolve-object-acl.d.ts +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +769 -2
- package/dist/index.js.map +1 -1
- package/dist/types/delete-hook-context.d.ts +5 -0
- package/dist/types/download-hook-context.d.ts +6 -0
- package/dist/types/download-success-context.d.ts +5 -0
- package/dist/types/hook-context.d.ts +3 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/multipart-complete-success-context.d.ts +13 -0
- package/dist/types/multipart-hook-context.d.ts +7 -0
- package/dist/types/multipart-init-success-context.d.ts +8 -0
- package/dist/types/multipart-list-success-context.d.ts +11 -0
- package/dist/types/s3-handler-config.d.ts +66 -0
- package/dist/types/s3-handler.d.ts +1 -0
- package/dist/types/s3-handlers.d.ts +16 -0
- package/dist/types/s3-route-handler-config.d.ts +4 -0
- package/dist/types/upload-complete-context.d.ts +15 -0
- package/dist/types/upload-hook-context.d.ts +10 -0
- package/dist/types/upload-success-context.d.ts +5 -0
- package/dist/types.d.ts +1 -214
- package/package.json +1 -1
- package/dist/helpers/build-public-url.d.ts +0 -13
package/dist/adapters/next.js
CHANGED
|
@@ -1,3 +1,669 @@
|
|
|
1
|
-
import {createPresignedPost}from'@aws-sdk/s3-presigned-post';import {getSignedUrl}from'@aws-sdk/s3-request-presigner';import {PutObjectCommand,HeadObjectCommand,GetObjectCommand,DeleteObjectCommand,CreateMultipartUploadCommand,UploadPartCommand,ListPartsCommand,CompleteMultipartUploadCommand,AbortMultipartUploadCommand,GetObjectAclCommand}from'@aws-sdk/client-s3';async function b(e){try{let n=await e.json();return n&&typeof n=="object"?n:null}catch{return null}}function m(e,n){let t=typeof e=="string"?e.trim():"";return t||Response.json({message:`${n} is required`},{status:400})}function R(e){let n=Number(e);return Number.isFinite(n)&&n>0?Math.floor(n):600}function p(e){return async n=>{try{return await e(n)}catch(t){let r=t.code,s=r==="EAI_AGAIN"||r==="ECONNREFUSED"||r==="ECONNRESET"||r==="ETIMEDOUT"||r==="ENOTFOUND"?`S3 endpoint unreachable (${r}): check your endpoint URL and network connectivity`:t instanceof Error?t.message:"Internal server error";return console.error("[S3 API]",s),Response.json({message:s},{status:502})}}}async function d(e,n){if(!e)return null;try{return await e(n),null}catch(t){let r=t instanceof Error?t.message:"Forbidden",a=typeof t?.status=="number"?t.status:403;return Response.json({message:r},{status:a})}}function g(e){let n=e.replace(/[^\x20-\x7E]/g,"_").replace(/["\\]/g,"_"),t=encodeURIComponent(e);return `attachment; filename="${n}"; filename*=UTF-8''${t}`}function P(e,n,t,r){if(r!=="public-read"||!e)return;let a=e(n);if(a)return `${a.replace(/\/$/,"")}/${t}`}function T(e){if(!e)return;let n=e.match(/filename\*=UTF-8''([^;\s]+)/i);if(n)try{return decodeURIComponent(n[1])}catch{}return e.match(/filename="([^"]*)"/i)?.[1]}function M(e){return p(async n=>{let t=await b(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=m(t.key,"key");if(r instanceof Response)return r;let a=t.bucket?.trim()||e.defaultBucket,s=R(t.expiresIn),o=t.acl==="public-read"?"public-read":"private",i=t.contentType?.trim()||"application/octet-stream",u=typeof t.fileSize=="number"&&t.fileSize>0?Math.floor(t.fileSize):null,c=await d(e.upload?.presignGuard,{request:n,key:r,bucket:a,contentType:t.contentType,fileSize:u??void 0,metadata:t.metadata,acl:o});if(c)return c;let l=e.upload?.method??"post";if(l==="put"&&e.upload?.requireFileSize&&u===null)return Response.json({message:"fileSize is required when upload.requireFileSize is enabled"},{status:400});if(l==="put"){let w={"Content-Type":i};t.fileName&&(w["Content-Disposition"]=g(t.fileName));let S=await getSignedUrl(e.s3,new PutObjectCommand({Bucket:a,Key:r,ContentType:i,ACL:o,...t.fileName?{ContentDisposition:g(t.fileName)}:{},...u!==null?{ContentLength:u}:{}}),{expiresIn:s,...u!==null?{signableHeaders:new Set(["content-length"])}:{}});return await e.upload?.onPresigned?.({request:n,key:r,bucket:a,contentType:t.contentType,metadata:t.metadata,acl:o,url:S,expiresIn:s}),Response.json({bucket:a,key:r,url:S,headers:w,expiresIn:s,method:"put"})}let C={acl:o,"Content-Type":i};if(t.fileName&&(C["Content-Disposition"]=g(t.fileName)),t.metadata)for(let[w,S]of Object.entries(t.metadata))C[`x-amz-meta-${w}`]=S;let y=u??1,H=u??void 0,{url:h,fields:j}=await createPresignedPost(e.s3,{Bucket:a,Key:r,Conditions:H!==void 0?[["content-length-range",y,H]]:[["content-length-range",y,Number.MAX_SAFE_INTEGER]],Fields:C,Expires:s});return await e.upload?.onPresigned?.({request:n,key:r,bucket:a,contentType:t.contentType,metadata:t.metadata,acl:o,url:h,expiresIn:s}),Response.json({bucket:a,key:r,url:h,fields:j,expiresIn:s,method:"post"})})}async function N(e,n,t){try{return (await e.send(new GetObjectAclCommand({Bucket:n,Key:t}))).Grants?.some(s=>s.Grantee?.URI==="http://acs.amazonaws.com/groups/global/AllUsers"&&(s.Permission==="READ"||s.Permission==="FULL_CONTROL"))?"public-read":"private"}catch{return "private"}}function v(e){return p(async n=>{let t=await b(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=m(t.key,"key");if(r instanceof Response)return r;let a=t.bucket?.trim()||e.defaultBucket,s=await d(e.upload?.confirmGuard,{request:n,key:r,bucket:a});if(s)return s;let o=await e.s3.send(new HeadObjectCommand({Bucket:a,Key:r})),i=await N(e.s3,a,r),u=P(e.resolvePublicUrl,a,r,i),c=T(o.ContentDisposition),l={request:n,key:r,bucket:a,contentType:o.ContentType,contentLength:o.ContentLength??0,eTag:o.ETag?.replace(/"/g,""),metadata:o.Metadata,acl:i,fileName:c,publicUrl:u,versionId:o.VersionId,lastModified:o.LastModified?.toISOString()};return await e.upload?.onUploadConfirmed?.(l),Response.json({key:r,bucket:a,contentType:l.contentType,contentLength:l.contentLength,eTag:l.eTag,metadata:l.metadata??{},acl:i,fileName:c,publicUrl:u,versionId:l.versionId,lastModified:l.lastModified})})}function B(e){return p(async n=>{let{searchParams:t}=new URL(n.url),r=t.get("key")?.trim();if(!r)return Response.json({message:"key query parameter is required"},{status:400});let a=t.get("bucket")?.trim()||e.defaultBucket,s=R(t.get("expiresIn")),o=t.get("fileName")?.trim(),i=await d(e.download?.presignGuard,{request:n,key:r,bucket:a,fileName:o||void 0});if(i)return i;try{await e.s3.send(new HeadObjectCommand({Bucket:a,Key:r}));}catch(c){let l=c?.name;if(l==="NoSuchKey"||l==="NotFound")return Response.json({message:"Object not found"},{status:404});throw c}let u=await getSignedUrl(e.s3,new GetObjectCommand({Bucket:a,Key:r,ResponseContentDisposition:o?g(o):"attachment"}),{expiresIn:s});return await e.download?.onPresigned?.({request:n,key:r,bucket:a,fileName:o||void 0,url:u,expiresIn:s}),Response.json({bucket:a,key:r,url:u,expiresIn:s})})}function z(e){return p(async n=>{let{searchParams:t}=new URL(n.url),r=t.get("key")?.trim();if(!r)return Response.json({message:"key query parameter is required"},{status:400});let a=t.get("bucket")?.trim()||e.defaultBucket,s=await d(e.delete?.deleteGuard,{request:n,key:r,bucket:a});if(s)return s;try{await e.s3.send(new HeadObjectCommand({Bucket:a,Key:r}));}catch{return Response.json({message:"Object not found"},{status:404})}return await e.s3.send(new DeleteObjectCommand({Bucket:a,Key:r})),await e.delete?.onDeleted?.({request:n,key:r,bucket:a}),Response.json({success:!0,bucket:a,key:r})})}function A(e){return p(async n=>{let t=await b(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=m(t.key,"key");if(r instanceof Response)return r;let a=t.bucket?.trim()||e.defaultBucket,s=t.acl==="public-read"?"public-read":"private",o=typeof t.fileSize=="number"&&t.fileSize>0?Math.floor(t.fileSize):void 0;if(e.multipart?.requireFileSize&&o===void 0)return Response.json({message:"fileSize is required when multipart.requireFileSize is enabled"},{status:400});let i=await d(e.multipart?.initGuard,{request:n,key:r,bucket:a,fileSize:o});if(i)return i;let{UploadId:u}=await e.s3.send(new CreateMultipartUploadCommand({Bucket:a,Key:r,ContentType:t.contentType,ContentDisposition:t.fileName?g(t.fileName):void 0,Metadata:t.metadata,ACL:s}));return await e.multipart?.onInit?.({request:n,key:r,bucket:a,uploadId:u,contentType:t.contentType,fileSize:o,metadata:t.metadata,acl:s}),Response.json({bucket:a,key:r,uploadId:u},{status:201})})}function L(e){return p(async n=>{let t=await b(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=m(t.key,"key");if(r instanceof Response)return r;let a=m(t.uploadId,"uploadId");if(a instanceof Response)return a;let s=Number(t.partNumber);if(!Number.isInteger(s)||s<=0)return Response.json({message:"partNumber must be a positive integer"},{status:400});let o=t.bucket?.trim()||e.defaultBucket,i=R(t.expiresIn),u=typeof t.partSize=="number"&&t.partSize>0?Math.floor(t.partSize):null,c=await d(e.multipart?.partGuard,{request:n,key:r,bucket:o});if(c)return c;let l=await getSignedUrl(e.s3,new UploadPartCommand({Bucket:o,Key:r,UploadId:a,PartNumber:s,...u!==null?{ContentLength:u}:{}}),{expiresIn:i,...u!==null?{signableHeaders:new Set(["content-length"])}:{}});return Response.json({presignedUrl:l,partNumber:s,uploadId:a,bucket:o,expiresIn:i,...u!==null?{partSize:u}:{}})})}function F(e){return p(async n=>{let t=await b(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=m(t.key,"key");if(r instanceof Response)return r;let a=m(t.uploadId,"uploadId");if(a instanceof Response)return a;let s=(Array.isArray(t.parts)?t.parts:[]).map(({partNumber:f})=>Number(f)).filter(f=>Number.isInteger(f)&&f>0).sort((f,I)=>f-I);if(!s.length)return Response.json({message:"At least one valid part is required"},{status:400});let o=t.bucket?.trim()||e.defaultBucket,i=await d(e.multipart?.completeGuard,{request:n,key:r,bucket:o});if(i)return i;let c=(await e.s3.send(new ListPartsCommand({Bucket:o,Key:r,UploadId:a}))).Parts??[],l=s.map(f=>{let I=c.find(q=>q.PartNumber===f);return {PartNumber:f,ETag:I?.ETag??""}}),C=await e.s3.send(new CompleteMultipartUploadCommand({Bucket:o,Key:r,UploadId:a,MultipartUpload:{Parts:l}})),y=await e.s3.send(new HeadObjectCommand({Bucket:o,Key:r}));for(let f=0;f<4&&!y.ContentLength;f++)await new Promise(I=>setTimeout(I,250*2**f)),y=await e.s3.send(new HeadObjectCommand({Bucket:o,Key:r}));let H=y.ContentLength??0,h=y.ContentType,j=(y.ETag??C.ETag??"").replace(/"/g,""),w=y.Metadata??{},S=y.VersionId,E=y.LastModified?.toISOString(),x=await N(e.s3,o,r),U=P(e.resolvePublicUrl,o,r,x),O=T(y.ContentDisposition);return await e.multipart?.onComplete?.({request:n,key:r,bucket:o,uploadId:a,contentLength:H,contentType:h,eTag:j,metadata:w,acl:x,fileName:O,publicUrl:U,versionId:S,lastModified:E}),Response.json({bucket:o,key:r,uploadId:a,contentLength:H,contentType:h,eTag:j,metadata:w,acl:x,fileName:O,publicUrl:U,versionId:S,lastModified:E})})}function K(e){return p(async n=>{let t=await b(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=m(t.key,"key");if(r instanceof Response)return r;let a=m(t.uploadId,"uploadId");if(a instanceof Response)return a;let s=t.bucket?.trim()||e.defaultBucket,o=await d(e.multipart?.abortGuard,{request:n,key:r,bucket:s});return o||(await e.s3.send(new AbortMultipartUploadCommand({Bucket:s,Key:r,UploadId:a})),await e.multipart?.onAbort?.({request:n,key:r,bucket:s,uploadId:a}),Response.json({bucket:s,key:r,uploadId:a,aborted:!0}))})}function G(e){return {presign:{upload:M(e),confirm:v(e),download:B(e)},multipart:{init:A(e),part:L(e),complete:F(e),abort:K(e)},delete:z(e)}}var k=()=>Response.json({message:"Not Found"},{status:404});function $(e){let n=G(e),t=e.basePath.replace(/\/$/,"");return async r=>{let a=await d(e.guard,{request:r});if(a)return a;let o=new URL(r.url).pathname.slice(t.length).replace(/^\//,""),i=r.method;return i==="POST"&&o==="presign/upload"?e.upload?.enabled?n.presign.upload(r):k():i==="POST"&&o==="presign/upload/confirm"?e.upload?.enabled?n.presign.confirm(r):k():i==="GET"&&o==="presign/download"?e.download?.enabled?n.presign.download(r):k():i==="DELETE"&&o==="delete"?e.delete?.enabled?n.delete(r):k():i==="POST"&&o==="presign/multipart/init"?e.multipart?.enabled?n.multipart.init(r):k():i==="POST"&&o==="presign/multipart/part"?e.multipart?.enabled?n.multipart.part(r):k():i==="POST"&&o==="presign/multipart/complete"?e.multipart?.enabled?n.multipart.complete(r):k():i==="POST"&&o==="presign/multipart/abort"?e.multipart?.enabled?n.multipart.abort(r):k():Response.json({message:"Not Found"},{status:404})}}function ft(e){return $(e)}
|
|
2
|
-
|
|
1
|
+
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
2
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
3
|
+
import { PutObjectCommand, HeadObjectCommand, GetObjectCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, ListPartsCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, GetObjectAclCommand } from '@aws-sdk/client-s3';
|
|
4
|
+
|
|
5
|
+
// src/handlers/presign/upload.ts
|
|
6
|
+
|
|
7
|
+
// src/internal-helpers.ts
|
|
8
|
+
async function parseBody(request) {
|
|
9
|
+
try {
|
|
10
|
+
const body = await request.json();
|
|
11
|
+
return body && typeof body === "object" ? body : null;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function requireString(value, name) {
|
|
17
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return Response.json({ message: `${name} is required` }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
function normalizeExpiresIn(value) {
|
|
24
|
+
const n = Number(value);
|
|
25
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
|
|
26
|
+
}
|
|
27
|
+
function withS3ErrorHandler(handler) {
|
|
28
|
+
return async (request) => {
|
|
29
|
+
try {
|
|
30
|
+
return await handler(request);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const code = err.code;
|
|
33
|
+
const isNetworkError = code === "EAI_AGAIN" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND";
|
|
34
|
+
const message = isNetworkError ? `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity` : err instanceof Error ? err.message : "Internal server error";
|
|
35
|
+
console.error("[S3 API]", message);
|
|
36
|
+
return Response.json({ message }, { status: 502 });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function runHook(hook, context) {
|
|
41
|
+
if (!hook) return null;
|
|
42
|
+
try {
|
|
43
|
+
await hook(context);
|
|
44
|
+
return null;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : "Forbidden";
|
|
47
|
+
const status = typeof err?.status === "number" ? err.status : 403;
|
|
48
|
+
return Response.json({ message }, { status });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/helpers/build-content-disposition.ts
|
|
53
|
+
function buildContentDisposition(fileName) {
|
|
54
|
+
const ascii = fileName.replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "_");
|
|
55
|
+
const encoded = encodeURIComponent(fileName);
|
|
56
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/helpers/parse-file-name.ts
|
|
60
|
+
function parseFileName(contentDisposition) {
|
|
61
|
+
if (!contentDisposition) return void 0;
|
|
62
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;\s]+)/i);
|
|
63
|
+
if (utf8Match) {
|
|
64
|
+
try {
|
|
65
|
+
return decodeURIComponent(utf8Match[1]);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const asciiMatch = contentDisposition.match(/filename="([^"]*)"/i);
|
|
70
|
+
return asciiMatch?.[1];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/handlers/presign/upload.ts
|
|
74
|
+
function createUploadHandler(config) {
|
|
75
|
+
return withS3ErrorHandler(async (request) => {
|
|
76
|
+
const body = await parseBody(request);
|
|
77
|
+
if (!body) {
|
|
78
|
+
return Response.json(
|
|
79
|
+
{ message: "Invalid JSON payload" },
|
|
80
|
+
{ status: 400 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const key = requireString(body.key, "key");
|
|
84
|
+
if (key instanceof Response) return key;
|
|
85
|
+
const bucket = body.bucket?.trim() || config.defaultBucket;
|
|
86
|
+
const expiresIn = normalizeExpiresIn(body.expiresIn);
|
|
87
|
+
const acl = body.acl === "public-read" ? "public-read" : "private";
|
|
88
|
+
const contentType = body.contentType?.trim() || "application/octet-stream";
|
|
89
|
+
const fileSize = typeof body.fileSize === "number" && body.fileSize > 0 ? Math.floor(body.fileSize) : null;
|
|
90
|
+
const guardResult = await runHook(config.upload?.presignGuard, {
|
|
91
|
+
request,
|
|
92
|
+
key,
|
|
93
|
+
bucket,
|
|
94
|
+
contentType: body.contentType,
|
|
95
|
+
fileSize: fileSize ?? void 0,
|
|
96
|
+
metadata: body.metadata,
|
|
97
|
+
acl
|
|
98
|
+
});
|
|
99
|
+
if (guardResult) return guardResult;
|
|
100
|
+
const method = config.upload?.method ?? "post";
|
|
101
|
+
if (method === "put" && config.upload?.requireFileSize && fileSize === null) {
|
|
102
|
+
return Response.json(
|
|
103
|
+
{
|
|
104
|
+
message: "fileSize is required when upload.requireFileSize is enabled"
|
|
105
|
+
},
|
|
106
|
+
{ status: 400 }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (method === "put") {
|
|
110
|
+
const putHeaders = {
|
|
111
|
+
"Content-Type": contentType
|
|
112
|
+
};
|
|
113
|
+
if (body.fileName) {
|
|
114
|
+
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
115
|
+
body.fileName
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const url2 = await getSignedUrl(
|
|
119
|
+
config.s3,
|
|
120
|
+
new PutObjectCommand({
|
|
121
|
+
Bucket: bucket,
|
|
122
|
+
Key: key,
|
|
123
|
+
ContentType: contentType,
|
|
124
|
+
ACL: acl,
|
|
125
|
+
...body.fileName ? { ContentDisposition: buildContentDisposition(body.fileName) } : {},
|
|
126
|
+
...fileSize !== null ? { ContentLength: fileSize } : {}
|
|
127
|
+
}),
|
|
128
|
+
{
|
|
129
|
+
expiresIn,
|
|
130
|
+
// Force content-length into X-Amz-SignedHeaders. Without this the
|
|
131
|
+
// SDK omits it from the signed header set and size enforcement is lost.
|
|
132
|
+
...fileSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
await config.upload?.onPresigned?.({
|
|
136
|
+
request,
|
|
137
|
+
key,
|
|
138
|
+
bucket,
|
|
139
|
+
contentType: body.contentType,
|
|
140
|
+
metadata: body.metadata,
|
|
141
|
+
acl,
|
|
142
|
+
url: url2,
|
|
143
|
+
expiresIn
|
|
144
|
+
});
|
|
145
|
+
return Response.json({
|
|
146
|
+
bucket,
|
|
147
|
+
key,
|
|
148
|
+
url: url2,
|
|
149
|
+
headers: putHeaders,
|
|
150
|
+
expiresIn,
|
|
151
|
+
method: "put"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const fields = { acl, "Content-Type": contentType };
|
|
155
|
+
if (body.fileName) {
|
|
156
|
+
fields["Content-Disposition"] = buildContentDisposition(body.fileName);
|
|
157
|
+
}
|
|
158
|
+
if (body.metadata) {
|
|
159
|
+
for (const [k, v] of Object.entries(body.metadata)) {
|
|
160
|
+
fields[`x-amz-meta-${k}`] = v;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const rangeMin = fileSize ?? 1;
|
|
164
|
+
const rangeMax = fileSize ?? void 0;
|
|
165
|
+
const { url, fields: signedFields } = await createPresignedPost(config.s3, {
|
|
166
|
+
Bucket: bucket,
|
|
167
|
+
Key: key,
|
|
168
|
+
Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
|
|
169
|
+
Fields: fields,
|
|
170
|
+
Expires: expiresIn
|
|
171
|
+
});
|
|
172
|
+
await config.upload?.onPresigned?.({
|
|
173
|
+
request,
|
|
174
|
+
key,
|
|
175
|
+
bucket,
|
|
176
|
+
contentType: body.contentType,
|
|
177
|
+
metadata: body.metadata,
|
|
178
|
+
acl,
|
|
179
|
+
url,
|
|
180
|
+
expiresIn
|
|
181
|
+
});
|
|
182
|
+
return Response.json({
|
|
183
|
+
bucket,
|
|
184
|
+
key,
|
|
185
|
+
url,
|
|
186
|
+
fields: signedFields,
|
|
187
|
+
expiresIn,
|
|
188
|
+
method: "post"
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
193
|
+
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
196
|
+
try {
|
|
197
|
+
const result = await s3.send(
|
|
198
|
+
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
199
|
+
{ abortSignal: controller.signal }
|
|
200
|
+
);
|
|
201
|
+
const isPublic = result.Grants?.some(
|
|
202
|
+
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
203
|
+
);
|
|
204
|
+
return isPublic ? "public-read" : "private";
|
|
205
|
+
} catch {
|
|
206
|
+
return void 0;
|
|
207
|
+
} finally {
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/handlers/presign/confirm.ts
|
|
213
|
+
function createConfirmHandler(config) {
|
|
214
|
+
return withS3ErrorHandler(async (request) => {
|
|
215
|
+
const body = await parseBody(request);
|
|
216
|
+
if (!body) {
|
|
217
|
+
return Response.json(
|
|
218
|
+
{ message: "Invalid JSON payload" },
|
|
219
|
+
{ status: 400 }
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const key = requireString(body.key, "key");
|
|
223
|
+
if (key instanceof Response) return key;
|
|
224
|
+
const bucket = body.bucket?.trim() || config.defaultBucket;
|
|
225
|
+
const guardResult = await runHook(config.upload?.confirmGuard, {
|
|
226
|
+
request,
|
|
227
|
+
key,
|
|
228
|
+
bucket
|
|
229
|
+
});
|
|
230
|
+
if (guardResult) return guardResult;
|
|
231
|
+
const head = await config.s3.send(
|
|
232
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
233
|
+
);
|
|
234
|
+
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
|
|
235
|
+
const fileName = parseFileName(head.ContentDisposition);
|
|
236
|
+
const context = {
|
|
237
|
+
request,
|
|
238
|
+
key,
|
|
239
|
+
bucket,
|
|
240
|
+
contentType: head.ContentType,
|
|
241
|
+
contentLength: head.ContentLength ?? 0,
|
|
242
|
+
eTag: head.ETag?.replace(/"/g, ""),
|
|
243
|
+
metadata: head.Metadata,
|
|
244
|
+
acl,
|
|
245
|
+
fileName,
|
|
246
|
+
versionId: head.VersionId,
|
|
247
|
+
lastModified: head.LastModified?.toISOString()
|
|
248
|
+
};
|
|
249
|
+
await config.upload?.onUploadConfirmed?.(context);
|
|
250
|
+
return Response.json({
|
|
251
|
+
key,
|
|
252
|
+
bucket,
|
|
253
|
+
contentType: context.contentType,
|
|
254
|
+
contentLength: context.contentLength,
|
|
255
|
+
eTag: context.eTag,
|
|
256
|
+
metadata: context.metadata ?? {},
|
|
257
|
+
acl,
|
|
258
|
+
fileName,
|
|
259
|
+
versionId: context.versionId,
|
|
260
|
+
lastModified: context.lastModified
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function createDownloadHandler(config) {
|
|
265
|
+
return withS3ErrorHandler(async (request) => {
|
|
266
|
+
const { searchParams } = new URL(request.url);
|
|
267
|
+
const key = searchParams.get("key")?.trim();
|
|
268
|
+
if (!key) {
|
|
269
|
+
return Response.json(
|
|
270
|
+
{ message: "key query parameter is required" },
|
|
271
|
+
{ status: 400 }
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const bucket = searchParams.get("bucket")?.trim() || config.defaultBucket;
|
|
275
|
+
const expiresIn = normalizeExpiresIn(searchParams.get("expiresIn"));
|
|
276
|
+
const fileName = searchParams.get("fileName")?.trim();
|
|
277
|
+
const guardResult = await runHook(config.download?.presignGuard, {
|
|
278
|
+
request,
|
|
279
|
+
key,
|
|
280
|
+
bucket,
|
|
281
|
+
fileName: fileName || void 0
|
|
282
|
+
});
|
|
283
|
+
if (guardResult) return guardResult;
|
|
284
|
+
try {
|
|
285
|
+
await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const name = err?.name;
|
|
288
|
+
if (name === "NoSuchKey" || name === "NotFound") {
|
|
289
|
+
return Response.json({ message: "Object not found" }, { status: 404 });
|
|
290
|
+
}
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
const url = await getSignedUrl(
|
|
294
|
+
config.s3,
|
|
295
|
+
new GetObjectCommand({
|
|
296
|
+
Bucket: bucket,
|
|
297
|
+
Key: key,
|
|
298
|
+
ResponseContentDisposition: fileName ? buildContentDisposition(fileName) : "attachment"
|
|
299
|
+
}),
|
|
300
|
+
{ expiresIn }
|
|
301
|
+
);
|
|
302
|
+
await config.download?.onPresigned?.({
|
|
303
|
+
request,
|
|
304
|
+
key,
|
|
305
|
+
bucket,
|
|
306
|
+
fileName: fileName || void 0,
|
|
307
|
+
url,
|
|
308
|
+
expiresIn
|
|
309
|
+
});
|
|
310
|
+
return Response.json({ bucket, key, url, expiresIn });
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function createDeleteHandler(config) {
|
|
314
|
+
return withS3ErrorHandler(async (request) => {
|
|
315
|
+
const { searchParams } = new URL(request.url);
|
|
316
|
+
const key = searchParams.get("key")?.trim();
|
|
317
|
+
if (!key) {
|
|
318
|
+
return Response.json(
|
|
319
|
+
{ message: "key query parameter is required" },
|
|
320
|
+
{ status: 400 }
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const bucket = searchParams.get("bucket")?.trim() || config.defaultBucket;
|
|
324
|
+
const guardResult = await runHook(config.delete?.deleteGuard, {
|
|
325
|
+
request,
|
|
326
|
+
key,
|
|
327
|
+
bucket
|
|
328
|
+
});
|
|
329
|
+
if (guardResult) return guardResult;
|
|
330
|
+
try {
|
|
331
|
+
await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
332
|
+
} catch {
|
|
333
|
+
return Response.json({ message: "Object not found" }, { status: 404 });
|
|
334
|
+
}
|
|
335
|
+
await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
336
|
+
await config.delete?.onDeleted?.({ request, key, bucket });
|
|
337
|
+
return Response.json({ success: true, bucket, key });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function createMultipartInitHandler(config) {
|
|
341
|
+
return withS3ErrorHandler(async (request) => {
|
|
342
|
+
const body = await parseBody(request);
|
|
343
|
+
if (!body) {
|
|
344
|
+
return Response.json(
|
|
345
|
+
{ message: "Invalid JSON payload" },
|
|
346
|
+
{ status: 400 }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const key = requireString(body.key, "key");
|
|
350
|
+
if (key instanceof Response) return key;
|
|
351
|
+
const bucket = body.bucket?.trim() || config.defaultBucket;
|
|
352
|
+
const acl = body.acl === "public-read" ? "public-read" : "private";
|
|
353
|
+
const fileSize = typeof body.fileSize === "number" && body.fileSize > 0 ? Math.floor(body.fileSize) : void 0;
|
|
354
|
+
if (config.multipart?.requireFileSize && fileSize === void 0) {
|
|
355
|
+
return Response.json(
|
|
356
|
+
{
|
|
357
|
+
message: "fileSize is required when multipart.requireFileSize is enabled"
|
|
358
|
+
},
|
|
359
|
+
{ status: 400 }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const guardResult = await runHook(config.multipart?.initGuard, {
|
|
363
|
+
request,
|
|
364
|
+
key,
|
|
365
|
+
bucket,
|
|
366
|
+
fileSize
|
|
367
|
+
});
|
|
368
|
+
if (guardResult) return guardResult;
|
|
369
|
+
const { UploadId } = await config.s3.send(
|
|
370
|
+
new CreateMultipartUploadCommand({
|
|
371
|
+
Bucket: bucket,
|
|
372
|
+
Key: key,
|
|
373
|
+
ContentType: body.contentType,
|
|
374
|
+
ContentDisposition: body.fileName ? buildContentDisposition(body.fileName) : void 0,
|
|
375
|
+
Metadata: body.metadata,
|
|
376
|
+
ACL: acl
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
await config.multipart?.onInit?.({
|
|
380
|
+
request,
|
|
381
|
+
key,
|
|
382
|
+
bucket,
|
|
383
|
+
uploadId: UploadId,
|
|
384
|
+
contentType: body.contentType,
|
|
385
|
+
fileSize,
|
|
386
|
+
metadata: body.metadata,
|
|
387
|
+
acl
|
|
388
|
+
});
|
|
389
|
+
return Response.json({ bucket, key, uploadId: UploadId }, { status: 201 });
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function createMultipartPartHandler(config) {
|
|
393
|
+
return withS3ErrorHandler(async (request) => {
|
|
394
|
+
const body = await parseBody(request);
|
|
395
|
+
if (!body) {
|
|
396
|
+
return Response.json(
|
|
397
|
+
{ message: "Invalid JSON payload" },
|
|
398
|
+
{ status: 400 }
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const key = requireString(body.key, "key");
|
|
402
|
+
if (key instanceof Response) return key;
|
|
403
|
+
const uploadId = requireString(body.uploadId, "uploadId");
|
|
404
|
+
if (uploadId instanceof Response) return uploadId;
|
|
405
|
+
const partNumber = Number(body.partNumber);
|
|
406
|
+
if (!Number.isInteger(partNumber) || partNumber <= 0) {
|
|
407
|
+
return Response.json(
|
|
408
|
+
{ message: "partNumber must be a positive integer" },
|
|
409
|
+
{ status: 400 }
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const bucket = body.bucket?.trim() || config.defaultBucket;
|
|
413
|
+
const expiresIn = normalizeExpiresIn(body.expiresIn);
|
|
414
|
+
const partSize = typeof body.partSize === "number" && body.partSize > 0 ? Math.floor(body.partSize) : null;
|
|
415
|
+
const guardResult = await runHook(config.multipart?.partGuard, {
|
|
416
|
+
request,
|
|
417
|
+
key,
|
|
418
|
+
bucket
|
|
419
|
+
});
|
|
420
|
+
if (guardResult) return guardResult;
|
|
421
|
+
const presignedUrl = await getSignedUrl(
|
|
422
|
+
config.s3,
|
|
423
|
+
new UploadPartCommand({
|
|
424
|
+
Bucket: bucket,
|
|
425
|
+
Key: key,
|
|
426
|
+
UploadId: uploadId,
|
|
427
|
+
PartNumber: partNumber,
|
|
428
|
+
...partSize !== null ? { ContentLength: partSize } : {}
|
|
429
|
+
}),
|
|
430
|
+
{
|
|
431
|
+
expiresIn,
|
|
432
|
+
...partSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
return Response.json({
|
|
436
|
+
presignedUrl,
|
|
437
|
+
partNumber,
|
|
438
|
+
uploadId,
|
|
439
|
+
bucket,
|
|
440
|
+
expiresIn,
|
|
441
|
+
...partSize !== null ? { partSize } : {}
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
function createMultipartCompleteHandler(config) {
|
|
446
|
+
return withS3ErrorHandler(async (request) => {
|
|
447
|
+
const body = await parseBody(request);
|
|
448
|
+
if (!body) {
|
|
449
|
+
return Response.json(
|
|
450
|
+
{ message: "Invalid JSON payload" },
|
|
451
|
+
{ status: 400 }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const key = requireString(body.key, "key");
|
|
455
|
+
if (key instanceof Response) return key;
|
|
456
|
+
const uploadId = requireString(body.uploadId, "uploadId");
|
|
457
|
+
if (uploadId instanceof Response) return uploadId;
|
|
458
|
+
const parts = (Array.isArray(body.parts) ? body.parts : []).map(({ partNumber }) => Number(partNumber)).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
|
|
459
|
+
if (!parts.length) {
|
|
460
|
+
return Response.json(
|
|
461
|
+
{ message: "At least one valid part is required" },
|
|
462
|
+
{ status: 400 }
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const bucket = body.bucket?.trim() || config.defaultBucket;
|
|
466
|
+
const guardResult = await runHook(config.multipart?.completeGuard, {
|
|
467
|
+
request,
|
|
468
|
+
key,
|
|
469
|
+
bucket
|
|
470
|
+
});
|
|
471
|
+
if (guardResult) return guardResult;
|
|
472
|
+
const listed = await config.s3.send(
|
|
473
|
+
new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
|
|
474
|
+
);
|
|
475
|
+
const listedParts = listed.Parts ?? [];
|
|
476
|
+
const completeParts = parts.map((partNumber) => {
|
|
477
|
+
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
478
|
+
return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
|
|
479
|
+
});
|
|
480
|
+
const completeResult = await config.s3.send(
|
|
481
|
+
new CompleteMultipartUploadCommand({
|
|
482
|
+
Bucket: bucket,
|
|
483
|
+
Key: key,
|
|
484
|
+
UploadId: uploadId,
|
|
485
|
+
MultipartUpload: { Parts: completeParts }
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
let head = await config.s3.send(
|
|
489
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
490
|
+
);
|
|
491
|
+
for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
|
|
492
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
493
|
+
head = await config.s3.send(
|
|
494
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
const contentLength = head.ContentLength ?? 0;
|
|
498
|
+
const contentType = head.ContentType;
|
|
499
|
+
const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
|
|
500
|
+
const metadata = head.Metadata ?? {};
|
|
501
|
+
const versionId = head.VersionId;
|
|
502
|
+
const lastModified = head.LastModified?.toISOString();
|
|
503
|
+
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
|
|
504
|
+
const fileName = parseFileName(head.ContentDisposition);
|
|
505
|
+
await config.multipart?.onComplete?.({
|
|
506
|
+
request,
|
|
507
|
+
key,
|
|
508
|
+
bucket,
|
|
509
|
+
uploadId,
|
|
510
|
+
contentLength,
|
|
511
|
+
contentType,
|
|
512
|
+
eTag,
|
|
513
|
+
metadata,
|
|
514
|
+
acl,
|
|
515
|
+
fileName,
|
|
516
|
+
versionId,
|
|
517
|
+
lastModified
|
|
518
|
+
});
|
|
519
|
+
return Response.json({
|
|
520
|
+
bucket,
|
|
521
|
+
key,
|
|
522
|
+
uploadId,
|
|
523
|
+
contentLength,
|
|
524
|
+
contentType,
|
|
525
|
+
eTag,
|
|
526
|
+
metadata,
|
|
527
|
+
acl,
|
|
528
|
+
fileName,
|
|
529
|
+
versionId,
|
|
530
|
+
lastModified
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function createMultipartAbortHandler(config) {
|
|
535
|
+
return withS3ErrorHandler(async (request) => {
|
|
536
|
+
const body = await parseBody(request);
|
|
537
|
+
if (!body) {
|
|
538
|
+
return Response.json(
|
|
539
|
+
{ message: "Invalid JSON payload" },
|
|
540
|
+
{ status: 400 }
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const key = requireString(body.key, "key");
|
|
544
|
+
if (key instanceof Response) return key;
|
|
545
|
+
const uploadId = requireString(body.uploadId, "uploadId");
|
|
546
|
+
if (uploadId instanceof Response) return uploadId;
|
|
547
|
+
const bucket = body.bucket?.trim() || config.defaultBucket;
|
|
548
|
+
const guardResult = await runHook(config.multipart?.abortGuard, {
|
|
549
|
+
request,
|
|
550
|
+
key,
|
|
551
|
+
bucket
|
|
552
|
+
});
|
|
553
|
+
if (guardResult) return guardResult;
|
|
554
|
+
await config.s3.send(
|
|
555
|
+
new AbortMultipartUploadCommand({
|
|
556
|
+
Bucket: bucket,
|
|
557
|
+
Key: key,
|
|
558
|
+
UploadId: uploadId
|
|
559
|
+
})
|
|
560
|
+
);
|
|
561
|
+
await config.multipart?.onAbort?.({
|
|
562
|
+
request,
|
|
563
|
+
key,
|
|
564
|
+
bucket,
|
|
565
|
+
uploadId
|
|
566
|
+
});
|
|
567
|
+
return Response.json({ bucket, key, uploadId, aborted: true });
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function createMultipartListPartsHandler(config) {
|
|
571
|
+
return withS3ErrorHandler(async (request) => {
|
|
572
|
+
const url = new URL(request.url);
|
|
573
|
+
const key = url.searchParams.get("key")?.trim() ?? "";
|
|
574
|
+
const uploadId = url.searchParams.get("uploadId")?.trim() ?? "";
|
|
575
|
+
const bucketParam = url.searchParams.get("bucket")?.trim();
|
|
576
|
+
if (!key) {
|
|
577
|
+
return Response.json({ message: "key is required" }, { status: 400 });
|
|
578
|
+
}
|
|
579
|
+
if (!uploadId) {
|
|
580
|
+
return Response.json(
|
|
581
|
+
{ message: "uploadId is required" },
|
|
582
|
+
{ status: 400 }
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const bucket = bucketParam || config.defaultBucket;
|
|
586
|
+
const guardResult = await runHook(config.multipart?.listGuard, {
|
|
587
|
+
request,
|
|
588
|
+
key,
|
|
589
|
+
bucket,
|
|
590
|
+
uploadId
|
|
591
|
+
});
|
|
592
|
+
if (guardResult) return guardResult;
|
|
593
|
+
const response = await config.s3.send(
|
|
594
|
+
new ListPartsCommand({
|
|
595
|
+
Bucket: bucket,
|
|
596
|
+
Key: key,
|
|
597
|
+
UploadId: uploadId
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
const parts = (response.Parts ?? []).map((p) => ({
|
|
601
|
+
partNumber: p.PartNumber ?? 0,
|
|
602
|
+
size: p.Size ?? 0,
|
|
603
|
+
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
604
|
+
}));
|
|
605
|
+
await config.multipart?.onList?.({ request, key, bucket, uploadId, parts });
|
|
606
|
+
return Response.json({ parts });
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/create-handlers.ts
|
|
611
|
+
function createHandlers(config) {
|
|
612
|
+
return {
|
|
613
|
+
presign: {
|
|
614
|
+
upload: createUploadHandler(config),
|
|
615
|
+
confirm: createConfirmHandler(config),
|
|
616
|
+
download: createDownloadHandler(config)
|
|
617
|
+
},
|
|
618
|
+
multipart: {
|
|
619
|
+
init: createMultipartInitHandler(config),
|
|
620
|
+
part: createMultipartPartHandler(config),
|
|
621
|
+
complete: createMultipartCompleteHandler(config),
|
|
622
|
+
abort: createMultipartAbortHandler(config),
|
|
623
|
+
listParts: createMultipartListPartsHandler(config)
|
|
624
|
+
},
|
|
625
|
+
delete: createDeleteHandler(config)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/router.ts
|
|
630
|
+
var disabled = () => Response.json({ message: "Not Found" }, { status: 404 });
|
|
631
|
+
function createRouter(config) {
|
|
632
|
+
const handlers = createHandlers(config);
|
|
633
|
+
const base = config.basePath.replace(/\/$/, "");
|
|
634
|
+
return async (request) => {
|
|
635
|
+
const guardResult = await runHook(config.guard, { request });
|
|
636
|
+
if (guardResult) return guardResult;
|
|
637
|
+
const url = new URL(request.url);
|
|
638
|
+
const subpath = url.pathname.slice(base.length).replace(/^\//, "");
|
|
639
|
+
const method = request.method;
|
|
640
|
+
if (method === "POST" && subpath === "presign/upload")
|
|
641
|
+
return config.upload?.enabled ? handlers.presign.upload(request) : disabled();
|
|
642
|
+
if (method === "POST" && subpath === "presign/upload/confirm")
|
|
643
|
+
return config.upload?.enabled ? handlers.presign.confirm(request) : disabled();
|
|
644
|
+
if (method === "GET" && subpath === "presign/download")
|
|
645
|
+
return config.download?.enabled ? handlers.presign.download(request) : disabled();
|
|
646
|
+
if (method === "DELETE" && subpath === "delete")
|
|
647
|
+
return config.delete?.enabled ? handlers.delete(request) : disabled();
|
|
648
|
+
if (method === "POST" && subpath === "presign/multipart/init")
|
|
649
|
+
return config.multipart?.enabled ? handlers.multipart.init(request) : disabled();
|
|
650
|
+
if (method === "POST" && subpath === "presign/multipart/part")
|
|
651
|
+
return config.multipart?.enabled ? handlers.multipart.part(request) : disabled();
|
|
652
|
+
if (method === "POST" && subpath === "presign/multipart/complete")
|
|
653
|
+
return config.multipart?.enabled ? handlers.multipart.complete(request) : disabled();
|
|
654
|
+
if (method === "POST" && subpath === "presign/multipart/abort")
|
|
655
|
+
return config.multipart?.enabled ? handlers.multipart.abort(request) : disabled();
|
|
656
|
+
if (method === "GET" && subpath === "presign/multipart/parts")
|
|
657
|
+
return config.multipart?.enabled ? handlers.multipart.listParts(request) : disabled();
|
|
658
|
+
return Response.json({ message: "Not Found" }, { status: 404 });
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/adapters/next.ts
|
|
663
|
+
function createRouteHandler(config) {
|
|
664
|
+
return createRouter(config);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export { createRouteHandler };
|
|
668
|
+
//# sourceMappingURL=next.js.map
|
|
3
669
|
//# sourceMappingURL=next.js.map
|