speed up direct upload preparation

This commit is contained in:
134 2026-07-08 21:18:01 +08:00
parent 81a0b29633
commit 9575873efb
2 changed files with 69 additions and 1 deletions

View File

@ -29,6 +29,10 @@ const DEFAULT_PART_SIZE_BYTES = 8 * 1024 * 1024;
const PRESIGNED_URL_EXPIRES_SECONDS = 15 * 60;
const DIRECT_UPLOAD_PREFIX = "uploads/direct";
function elapsedMs(startedAt: number) {
return Date.now() - startedAt;
}
const allowedDirectUploadTypes = new Set([
"image/jpeg",
"image/jpg",
@ -141,6 +145,7 @@ async function createPartUrls(params: {
}
export async function POST(request: Request) {
const requestStartedAt = Date.now();
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@ -159,6 +164,16 @@ export async function POST(request: Request) {
);
}
logger.info("direct_upload_request_start", {
action: payload.action,
size: "size" in payload ? payload.size : null,
contentType: "contentType" in payload ? payload.contentType : null,
purpose: "purpose" in payload ? payload.purpose || null : null,
clientUploadId:
"clientUploadId" in payload ? payload.clientUploadId || null : null,
});
const resolveStartedAt = Date.now();
const target = await resolveS3UploadTarget().catch((error) => {
logger.error("resolve_storage_target_failed", error);
return null;
@ -170,6 +185,13 @@ export async function POST(request: Request) {
{ status: 409 }
);
}
logger.info("direct_upload_storage_target_resolved", {
action: payload.action,
elapsedMs: elapsedMs(resolveStartedAt),
provider: target.config.provider || "s3",
bucket: target.config.bucket,
hasPublicUrl: Boolean(target.config.publicUrl),
});
const client = createS3Client(target.config);
const bucket = target.config.bucket;
@ -203,6 +225,7 @@ export async function POST(request: Request) {
);
if (payload.size < MULTIPART_THRESHOLD_BYTES) {
const presignStartedAt = Date.now();
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
@ -213,6 +236,13 @@ export async function POST(request: Request) {
const url = await getSignedUrl(client, command, {
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS,
});
logger.info("direct_upload_single_presigned", {
elapsedMs: elapsedMs(presignStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
size: payload.size,
contentType,
clientUploadId: payload.clientUploadId || null,
});
const headers: Record<string, string> = {
"Content-Type": contentType,
@ -232,6 +262,7 @@ export async function POST(request: Request) {
});
}
const multipartStartedAt = Date.now();
const createResult = await client.send(
new CreateMultipartUploadCommand({
Bucket: bucket,
@ -256,6 +287,15 @@ export async function POST(request: Request) {
uploadId,
partCount,
});
logger.info("direct_upload_multipart_prepared", {
elapsedMs: elapsedMs(multipartStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
size: payload.size,
partCount,
partSize,
contentType,
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json({
mode: "multipart",
@ -271,6 +311,7 @@ export async function POST(request: Request) {
if (payload.action === "abort") {
const key = assertUserOwnedKey(payload.key, session.user.id);
const abortStartedAt = Date.now();
await client.send(
new AbortMultipartUploadCommand({
Bucket: bucket,
@ -278,6 +319,11 @@ export async function POST(request: Request) {
UploadId: payload.uploadId,
})
);
logger.info("direct_upload_multipart_aborted", {
elapsedMs: elapsedMs(abortStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json({ ok: true });
}
@ -291,6 +337,7 @@ export async function POST(request: Request) {
}
if (payload.uploadId) {
const completeMultipartStartedAt = Date.now();
const parts = [...(payload.parts || [])].sort(
(a, b) => a.PartNumber - b.PartNumber
);
@ -311,14 +358,25 @@ export async function POST(request: Request) {
},
})
);
logger.info("direct_upload_multipart_completed", {
elapsedMs: elapsedMs(completeMultipartStartedAt),
partCount: parts.length,
clientUploadId: payload.clientUploadId || null,
});
}
const headStartedAt = Date.now();
const head = await client.send(
new HeadObjectCommand({
Bucket: bucket,
Key: key,
})
);
logger.info("direct_upload_head_object_completed", {
elapsedMs: elapsedMs(headStartedAt),
uploadMode: payload.uploadId ? "multipart" : "single",
clientUploadId: payload.clientUploadId || null,
});
const actualSize =
typeof head.ContentLength === "number" ? head.ContentLength : null;
@ -329,6 +387,7 @@ export async function POST(request: Request) {
const url = buildS3PublicUrl(target.config, key);
const resolvedContentType =
head.ContentType || contentType || getContentType(payload.filename);
const assetStartedAt = Date.now();
const asset = await upsertMediaAsset({
userId: session.user.id,
mediaType: "image",
@ -349,6 +408,13 @@ export async function POST(request: Request) {
clientUploadId: payload.clientUploadId || null,
},
});
logger.info("direct_upload_asset_registered", {
elapsedMs: elapsedMs(assetStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
uploadMode: payload.uploadId ? "multipart" : "single",
assetId: asset?.id ?? null,
clientUploadId: payload.clientUploadId || null,
});
await maybeEnqueueStorageMigrationTask({
uploaded: { url, pathname: key },

View File

@ -7,6 +7,7 @@ const SERVER_UPLOAD_MAX_BYTES = 50 * 1024 * 1024;
const DIRECT_UPLOAD_PART_CONCURRENCY = 4;
const DIRECT_UPLOAD_SUPPRESS_MS = 2 * 60 * 1000;
const CLIENT_UPLOAD_ID_FIELD = "clientUploadId";
const DIRECT_UPLOAD_INITIATE_TIMEOUT_MS = 3 * 1000;
const DIRECT_UPLOAD_ACTION_TIMEOUT_MS = 15 * 1000;
const DIRECT_UPLOAD_ABORT_TIMEOUT_MS = 8 * 1000;
const XHR_UPLOAD_TOTAL_TIMEOUT_MS = 6 * 60 * 1000;
@ -725,7 +726,8 @@ async function fetchDirectUpload(
purpose: file.purpose,
clientUploadId: file.clientUploadId,
},
signal
signal,
DIRECT_UPLOAD_INITIATE_TIMEOUT_MS
);
if (upload.mode === "single") {