speed up direct upload preparation
This commit is contained in:
parent
81a0b29633
commit
9575873efb
@ -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 },
|
||||||
|
|||||||
@ -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") {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user