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 PRESIGNED_URL_EXPIRES_SECONDS = 15 * 60;
const DIRECT_UPLOAD_PREFIX = "uploads/direct"; const DIRECT_UPLOAD_PREFIX = "uploads/direct";
function elapsedMs(startedAt: number) {
return Date.now() - startedAt;
}
const allowedDirectUploadTypes = new Set([ const allowedDirectUploadTypes = new Set([
"image/jpeg", "image/jpeg",
"image/jpg", "image/jpg",
@ -141,6 +145,7 @@ async function createPartUrls(params: {
} }
export async function POST(request: Request) { export async function POST(request: Request) {
const requestStartedAt = Date.now();
const session = await auth(); const session = await auth();
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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) => { const target = await resolveS3UploadTarget().catch((error) => {
logger.error("resolve_storage_target_failed", error); logger.error("resolve_storage_target_failed", error);
return null; return null;
@ -170,6 +185,13 @@ export async function POST(request: Request) {
{ status: 409 } { 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 client = createS3Client(target.config);
const bucket = target.config.bucket; const bucket = target.config.bucket;
@ -203,6 +225,7 @@ export async function POST(request: Request) {
); );
if (payload.size < MULTIPART_THRESHOLD_BYTES) { if (payload.size < MULTIPART_THRESHOLD_BYTES) {
const presignStartedAt = Date.now();
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: key, Key: key,
@ -213,6 +236,13 @@ export async function POST(request: Request) {
const url = await getSignedUrl(client, command, { const url = await getSignedUrl(client, command, {
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS, 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> = { const headers: Record<string, string> = {
"Content-Type": contentType, "Content-Type": contentType,
@ -232,6 +262,7 @@ export async function POST(request: Request) {
}); });
} }
const multipartStartedAt = Date.now();
const createResult = await client.send( const createResult = await client.send(
new CreateMultipartUploadCommand({ new CreateMultipartUploadCommand({
Bucket: bucket, Bucket: bucket,
@ -256,6 +287,15 @@ export async function POST(request: Request) {
uploadId, uploadId,
partCount, 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({ return NextResponse.json({
mode: "multipart", mode: "multipart",
@ -271,6 +311,7 @@ export async function POST(request: Request) {
if (payload.action === "abort") { if (payload.action === "abort") {
const key = assertUserOwnedKey(payload.key, session.user.id); const key = assertUserOwnedKey(payload.key, session.user.id);
const abortStartedAt = Date.now();
await client.send( await client.send(
new AbortMultipartUploadCommand({ new AbortMultipartUploadCommand({
Bucket: bucket, Bucket: bucket,
@ -278,6 +319,11 @@ export async function POST(request: Request) {
UploadId: payload.uploadId, UploadId: payload.uploadId,
}) })
); );
logger.info("direct_upload_multipart_aborted", {
elapsedMs: elapsedMs(abortStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });
} }
@ -291,6 +337,7 @@ export async function POST(request: Request) {
} }
if (payload.uploadId) { if (payload.uploadId) {
const completeMultipartStartedAt = Date.now();
const parts = [...(payload.parts || [])].sort( const parts = [...(payload.parts || [])].sort(
(a, b) => a.PartNumber - b.PartNumber (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( const head = await client.send(
new HeadObjectCommand({ new HeadObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: key, Key: key,
}) })
); );
logger.info("direct_upload_head_object_completed", {
elapsedMs: elapsedMs(headStartedAt),
uploadMode: payload.uploadId ? "multipart" : "single",
clientUploadId: payload.clientUploadId || null,
});
const actualSize = const actualSize =
typeof head.ContentLength === "number" ? head.ContentLength : null; typeof head.ContentLength === "number" ? head.ContentLength : null;
@ -329,6 +387,7 @@ export async function POST(request: Request) {
const url = buildS3PublicUrl(target.config, key); const url = buildS3PublicUrl(target.config, key);
const resolvedContentType = const resolvedContentType =
head.ContentType || contentType || getContentType(payload.filename); head.ContentType || contentType || getContentType(payload.filename);
const assetStartedAt = Date.now();
const asset = await upsertMediaAsset({ const asset = await upsertMediaAsset({
userId: session.user.id, userId: session.user.id,
mediaType: "image", mediaType: "image",
@ -349,6 +408,13 @@ export async function POST(request: Request) {
clientUploadId: payload.clientUploadId || null, 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({ await maybeEnqueueStorageMigrationTask({
uploaded: { url, pathname: key }, 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_PART_CONCURRENCY = 4;
const DIRECT_UPLOAD_SUPPRESS_MS = 2 * 60 * 1000; const DIRECT_UPLOAD_SUPPRESS_MS = 2 * 60 * 1000;
const CLIENT_UPLOAD_ID_FIELD = "clientUploadId"; 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_ACTION_TIMEOUT_MS = 15 * 1000;
const DIRECT_UPLOAD_ABORT_TIMEOUT_MS = 8 * 1000; const DIRECT_UPLOAD_ABORT_TIMEOUT_MS = 8 * 1000;
const XHR_UPLOAD_TOTAL_TIMEOUT_MS = 6 * 60 * 1000; const XHR_UPLOAD_TOTAL_TIMEOUT_MS = 6 * 60 * 1000;
@ -725,7 +726,8 @@ async function fetchDirectUpload(
purpose: file.purpose, purpose: file.purpose,
clientUploadId: file.clientUploadId, clientUploadId: file.clientUploadId,
}, },
signal signal,
DIRECT_UPLOAD_INITIATE_TIMEOUT_MS
); );
if (upload.mode === "single") { if (upload.mode === "single") {