diff --git a/src/app/api/documents/blob/upload/finalize/route.ts b/src/app/api/documents/blob/upload/finalize/route.ts index ea82ce4..8ab877f 100644 --- a/src/app/api/documents/blob/upload/finalize/route.ts +++ b/src/app/api/documents/blob/upload/finalize/route.ts @@ -10,6 +10,7 @@ import { headDocumentBlob, headTempDocumentBlob, isMissingBlobError, + isPreconditionFailed, isValidTempUploadToken, putTempDocumentFinalizeReceipt, } from '@/lib/server/documents/blobstore'; @@ -138,13 +139,18 @@ async function finalizeOne(input: { await headDocumentBlob(documentId, input.namespace); } catch (error) { if (!isMissingBlobError(error)) throw error; - await copyTempDocumentBlobToDocument( - input.upload.token, - input.userId, - documentId, - input.namespace, - temp.contentType, - ); + try { + await copyTempDocumentBlobToDocument( + input.upload.token, + input.userId, + documentId, + input.namespace, + temp.contentType, + { ifNoneMatch: true }, + ); + } catch (copyError) { + if (!isPreconditionFailed(copyError)) throw copyError; + } } const canonicalHead = await headDocumentBlob(documentId, input.namespace); @@ -194,14 +200,13 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'No valid uploads provided' }, { status: 400 }); } - const stored: BaseDocument[] = []; - for (const upload of uploads) { - stored.push(await finalizeOne({ + const stored = await Promise.all( + uploads.map((upload) => finalizeOne({ upload, userId, namespace, - })); - } + })), + ); return NextResponse.json({ stored }); } catch (error) { diff --git a/src/lib/server/documents/blobstore.ts b/src/lib/server/documents/blobstore.ts index b8fa988..36316c9 100644 --- a/src/lib/server/documents/blobstore.ts +++ b/src/lib/server/documents/blobstore.ts @@ -73,6 +73,12 @@ export function isValidDocumentId(id: string): boolean { return DOCUMENT_ID_REGEX.test(id); } +export function isPreconditionFailed(error: unknown): boolean { + if (!error || typeof error !== 'object') return false; + const maybe = error as { name?: string; $metadata?: { httpStatusCode?: number } }; + return maybe.$metadata?.httpStatusCode === 412 || maybe.name === 'PreconditionFailed'; +} + export function isValidTempUploadToken(token: string): boolean { return TEMP_UPLOAD_TOKEN_REGEX.test(token); } @@ -455,6 +461,7 @@ export async function copyTempDocumentBlobToDocument( documentId: string, namespace: string | null, contentType: string, + options?: { ifNoneMatch?: boolean }, ): Promise { const cfg = getS3Config(); const client = getS3ProxyClient(); @@ -466,6 +473,7 @@ export async function copyTempDocumentBlobToDocument( ContentType: contentType, MetadataDirective: 'REPLACE', ServerSideEncryption: 'AES256', + ...(options?.ifNoneMatch ? { IfNoneMatch: '*' } : {}), }), ); } diff --git a/src/lib/server/documents/register-upload.ts b/src/lib/server/documents/register-upload.ts index 6fe134e..f63daea 100644 --- a/src/lib/server/documents/register-upload.ts +++ b/src/lib/server/documents/register-upload.ts @@ -27,6 +27,14 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn const reusableParsedPdf = input.type === 'pdf' ? await findReusableParsedPdfResult(input.documentId) : null; + const parsedJsonKey = reusableParsedPdf?.parsedJsonKey ?? null; + const parseState = input.type === 'pdf' + ? stringifyDocumentParseState( + reusableParsedPdf + ? { status: 'ready', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION } + : { status: 'pending', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION }, + ) + : null; await db .insert(documents) @@ -38,14 +46,8 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn size: input.size, lastModified: input.lastModified, filePath: input.documentId, - parseState: input.type === 'pdf' - ? stringifyDocumentParseState( - reusableParsedPdf - ? { status: 'ready', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION } - : { status: 'pending', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION }, - ) - : null, - parsedJsonKey: reusableParsedPdf?.parsedJsonKey ?? null, + parseState, + parsedJsonKey, }) .onConflictDoUpdate({ target: [documents.id, documents.userId], @@ -55,14 +57,8 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn size: input.size, lastModified: input.lastModified, filePath: input.documentId, - parseState: input.type === 'pdf' - ? stringifyDocumentParseState( - reusableParsedPdf - ? { status: 'ready', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION } - : { status: 'pending', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION }, - ) - : null, - parsedJsonKey: reusableParsedPdf?.parsedJsonKey ?? null, + parseState, + parsedJsonKey, }, });