From 53c4c0ed6016beb8c3e9748e3c508002e4b8be63 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 24 Apr 2026 16:58:49 +0200 Subject: [PATCH] fix: prevent duplicate uploads and group asset commits --- app/api/upload/drive/route.ts | 10 ++ app/api/upload/git/route.ts | 136 ++++++++++-------- app/page.tsx | 2 +- components/UploadZone.tsx | 12 +- components/ui/Modal.tsx | 8 +- components/upload/ActionButtons.tsx | 28 ++-- components/upload/DriveErrorModal.tsx | 3 + components/upload/DriveStatusLine.tsx | 4 +- components/upload/OverwriteConfirmModal.tsx | 3 + hooks/useUploadOrchestrator.ts | 150 +++++++++++++------- lib/asset-classification.ts | 23 +++ lib/commit-message.ts | 51 +++++-- lib/prepare-git-assets.ts | 26 +++- lib/types.ts | 9 ++ lib/upload-lock.ts | 16 +++ 15 files changed, 329 insertions(+), 152 deletions(-) create mode 100644 lib/asset-classification.ts create mode 100644 lib/upload-lock.ts diff --git a/app/api/upload/drive/route.ts b/app/api/upload/drive/route.ts index 3b60f0a..64ea0a0 100644 --- a/app/api/upload/drive/route.ts +++ b/app/api/upload/drive/route.ts @@ -7,6 +7,7 @@ import { uploadFile, findNextVersion, } from '@/lib/nextcloud' +import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -56,6 +57,13 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: false, error: message }, { status: 400 }) } + if (!acquireUploadLock(folderName)) { + return NextResponse.json( + { success: false, error: 'Un upload est deja en cours pour ce dossier. Patientez quelques secondes.' }, + { status: 409 }, + ) + } + const basePath = process.env.NEXTCLOUD_BASE_PATH || 'Models' const vfFolderPath = `${basePath}/VF/${folderName}` @@ -95,5 +103,7 @@ export async function POST(req: NextRequest) { { success: false, error: `Drive echoue: ${message}` }, { status: 500 }, ) + } finally { + releaseUploadLock(folderName) } } diff --git a/app/api/upload/git/route.ts b/app/api/upload/git/route.ts index b4a6661..08e928d 100644 --- a/app/api/upload/git/route.ts +++ b/app/api/upload/git/route.ts @@ -5,6 +5,7 @@ import { getRemoteFolder, pushAllToGitHub } from '@/lib/github' import { buildCommitMessage } from '@/lib/commit-message' import { classifyFileChanges } from '@/lib/diff-files' import { prepareGitAssets } from '@/lib/prepare-git-assets' +import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -31,67 +32,82 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: false, error: message }, { status: 400 }) } - // --- Process files (compress model + textures for Git) --- - const { - filesToPush, - modelFilename, - compressed, - compressionError, - textureNames, - } = await prepareGitAssets({ folderName, parsedFiles }) - - // --- Detect existing files and classify changes --- - const folderPath = `public/models/${folderName}` - let remoteFileMap: Map - - try { - const remote = await getRemoteFolder(folderPath) - remoteFileMap = new Map(remote.files.map((f) => [f.name.toLowerCase(), f.size])) - } catch { - remoteFileMap = new Map() - } - - const isReplace = remoteFileMap.size > 0 - - const { fileChanges, changedFilesToPush, deletedFileNames, deletePaths } = - classifyFileChanges(filesToPush, remoteFileMap, folderPath) - - // If nothing changed, don't create an empty commit - if (changedFilesToPush.length === 0 && deletePaths.length === 0) { - return NextResponse.json({ - success: true, - folderName, - filesCount: 0, - compressed, - compressionError: compressionError || undefined, - message: 'Aucun fichier modifie — rien a envoyer.', - }) - } - - // --- Build commit message --- - const commitMessage = buildCommitMessage( - folderName, modelFilename, textureNames, - compressed, isReplace, fileChanges, deletedFileNames, - ) - - // --- Push all in one commit --- - try { - const { commitUrl } = await pushAllToGitHub(changedFilesToPush, deletePaths, commitMessage) - - return NextResponse.json({ - success: true, - folderName, - filesCount: changedFilesToPush.length, - compressed, - compressionError: compressionError || undefined, - message: `${changedFilesToPush.length} fichier(s) modifie(s) envoye(s) sur GitHub en un seul commit.`, - commitUrl, - }) - } catch (err) { - const message = err instanceof Error ? err.message : 'Erreur GitHub inconnue' + if (!acquireUploadLock(folderName)) { return NextResponse.json( - { success: false, error: `Push GitHub echoue: ${message}` }, - { status: 500 }, + { success: false, error: 'Un upload est deja en cours pour ce dossier. Patientez quelques secondes.' }, + { status: 409 }, ) } + + try { + // --- Process files (compress model + textures for Git) --- + const { + filesToPush, + modelFilename, + compressed, + compressionError, + assetSummaries, + } = await prepareGitAssets({ folderName, parsedFiles }) + + // --- Detect existing files and classify changes --- + const folderPath = `public/models/${folderName}` + let remoteFileMap: Map + + try { + const remote = await getRemoteFolder(folderPath) + remoteFileMap = new Map(remote.files.map((f) => [f.name.toLowerCase(), f.size])) + } catch { + remoteFileMap = new Map() + } + + const isReplace = remoteFileMap.size > 0 + + const { fileChanges, changedFilesToPush, deletedFileNames, deletePaths } = + classifyFileChanges(filesToPush, remoteFileMap, folderPath) + + // If nothing changed, don't create an empty commit + if (changedFilesToPush.length === 0 && deletePaths.length === 0) { + return NextResponse.json({ + success: true, + folderName, + filesCount: 0, + compressed, + compressionError: compressionError || undefined, + message: 'Aucun fichier modifie — rien a envoyer.', + }) + } + + // --- Build commit message --- + const commitMessage = buildCommitMessage( + folderName, + modelFilename, + assetSummaries, + isReplace, + fileChanges, + deletedFileNames, + ) + + // --- Push all in one commit --- + try { + const { commitUrl } = await pushAllToGitHub(changedFilesToPush, deletePaths, commitMessage) + + return NextResponse.json({ + success: true, + folderName, + filesCount: changedFilesToPush.length, + compressed, + compressionError: compressionError || undefined, + message: `${changedFilesToPush.length} fichier(s) modifie(s) envoye(s) sur GitHub en un seul commit.`, + commitUrl, + }) + } catch (err) { + const message = err instanceof Error ? err.message : 'Erreur GitHub inconnue' + return NextResponse.json( + { success: false, error: `Push GitHub echoue: ${message}` }, + { status: 500 }, + ) + } + } finally { + releaseUploadLock(folderName) + } } diff --git a/app/page.tsx b/app/page.tsx index 0b53327..8c68098 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -15,7 +15,7 @@ export default function Home() { -