本文档面向需要先完成一版最小可用上传下载闭环的团队,按上传授权、前端直传、确认入库和下载授权的顺序给出示例。
前端先请求业务侧 `/api/storage/presign-upload`,由后端决定 objectKey、有效期、权限边界和 requestId。
前端拿到 uploadUrl 后直接对存储服务发 PUT,请求体只传文件本体,不再经过业务服务器。
上传完成后,前端把 objectKey、文件名、大小和业务主键发回业务后端,由后端确认绑定关系并写入数据库。
后续查看、下载或回放文件时,前端不直接拼接访问地址,而是再次走 `/api/storage/presign-download` 获取短时授权。
这一层的主要职责是识别当前用户、校验请求体、生成 objectKey 和 uploadUrl,并把统一结果结构返回给前端。
export async function POST(request: Request) {
const user = await requireCurrentUser(request);
const body = await request.json();
const result = await createStoragePresignUploadDemo({
userId: user.id,
fileName: body.fileName,
contentType: body.contentType,
resourceType: body.resourceType ?? 'project-cover',
});
return Response.json(result);
}第一次请求自己的上传授权接口,第二次再把文件 PUT 到 uploadUrl。正式项目里,上传成功后通常还需要继续确认入库,而不是只把 objectKey 暂存在前端内存里。
async function uploadProjectFile(file: File, accessToken: string) {
const presignResponse = await fetch('/api/storage/presign-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
resourceType: 'project-cover',
}),
});
const presignResult = await presignResponse.json();
await fetch(presignResult.uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
});
await fetch('/api/storage/confirm-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
objectKey: presignResult.objectKey,
fileName: file.name,
contentType: file.type,
size: file.size,
resourceType: 'project-cover',
projectId: 'proj_xxx',
}),
});
return presignResult.objectKey;
}当前仓库里已经提供 `/api/storage/confirm-upload` 这条示例 route。它会先校验当前用户目录,再回查对象是否已经上传成功,最后返回统一的 asset 结果结构。默认示例不会直接写数据库,但已经把确认入库前需要完成的校验落到了代码里。
export async function POST(request: Request) {
const user = await requireCurrentUser(request);
const body = await request.json();
const result = await confirmStoragePresignUploadDemo({
userId: user.id,
objectKey: body.objectKey,
fileName: body.fileName,
contentType: body.contentType,
size: body.size,
resourceType: body.resourceType,
projectId: body.projectId,
});
return Response.json(result);
}{
"id": "asset_xxx",
"projectId": "proj_xxx",
"objectKey": "docs-demo/<user-id>/project-cover/2026/06/abc123-demo-cover.png",
"fileName": "demo-cover.png",
"contentType": "image/png",
"size": 248731,
"status": "completed",
"createdAt": "2026-06-10T08:00:00.000Z"
}export async function POST(request: Request) {
const user = await requireCurrentUser(request);
const body = await request.json();
const fileRecord = await findProjectAsset(body.assetId, user.id);
const result = await createStoragePresignDownloadDemo({
userId: user.id,
objectKey: fileRecord.objectKey,
});
return Response.json(result);
}async function createDownloadLink(assetId: string, accessToken: string) {
const response = await fetch('/api/storage/presign-download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
objectKey: assetId,
}),
});
return response.json();
}async function createDownloadLink(objectKey: string, accessToken: string) {
const response = await fetch('/api/storage/presign-download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
objectKey,
}),
});
return response.json();
}因为对象已经写进存储,只代表“文件存在”,不代表“业务上可见”。确认入库负责把对象路径和业务主键、用户权限、状态流转真正绑定起来。
因为这样可以避免前端随手传任意 objectKey。真正安全的做法是前端传业务记录 ID,由后端查出对应 objectKey 后再决定是否签发下载。
是的,仓库里现在已经有 `/api/storage/confirm-upload` 这条 demo route。它会校验目录范围并确认对象存在,再返回统一的 asset 结构;正式产品版可在此基础上继续接入数据库写入。
文档中心帮助你完成接入,客户服务页帮助你了解采购、支持与服务信息。