import { NextRequest, NextResponse } from 'next/server' import { join } from 'path' import { mkdir, writeFile, readFile, unlink, rm } from 'fs/promises' import { existsSync } from 'fs' import { validateUploadSecret } from '@/lib/auth' import { parseMultiUpload } from '@/lib/parse-upload' import { compressWithBlender } from '@/lib/blender' import { getRemoteFolder, pushAllToGitHub } from '@/lib/github' import { buildCommitMessage } from '@/lib/commit-message' import { TMP_DIR } from '@/lib/constants' import type { FileChange } from '@/lib/types' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' /** * POST /api/upload/git * Upload files, compress with Blender, and push to GitHub via Octokit. */ export async function POST(req: NextRequest) { // --- Auth --- const authError = validateUploadSecret(req) if (authError) return authError // --- Parse all files --- let folderName: string let destination: string let parsedFiles: Awaited>['files'] try { const parsed = await parseMultiUpload(req) folderName = parsed.folderName destination = parsed.destination parsedFiles = parsed.files } catch (err) { const message = err instanceof Error ? err.message : 'Erreur inconnue' return NextResponse.json({ success: false, error: message }, { status: 400 }) } // --- Process files (compress model if possible) --- const filesToPush: { path: string; contentBase64: string }[] = [] let modelFilename = '' let compressed = false let compressionError: string | undefined const textureNames: string[] = [] for (const pf of parsedFiles) { let content = pf.buffer if (pf.isModel) { modelFilename = pf.filename // Write to /tmp for Blender compression const tmpFolder = join(TMP_DIR, folderName) await mkdir(tmpFolder, { recursive: true }) const tmpFilePath = join(tmpFolder, pf.filename) await writeFile(tmpFilePath, pf.buffer) const stem = pf.filename.replace(/\.[^.]+$/, '') const compressedPath = join(tmpFolder, `${stem}_compressed.glb`) try { const result = await compressWithBlender(tmpFilePath, compressedPath) if (result.success && existsSync(compressedPath)) { content = await readFile(compressedPath) compressed = true await unlink(compressedPath).catch(() => {}) } else { compressionError = result.error } } finally { // Always cleanup temp files await unlink(tmpFilePath).catch(() => {}) await rm(tmpFolder, { recursive: true, force: true }).catch(() => {}) } } else { textureNames.push(pf.filename) } filesToPush.push({ path: `public/models/${destination}/${folderName}/${pf.filename}`, contentBase64: content.toString('base64'), }) } // --- Detect existing files and compare size to classify changes (LFS-compatible) --- const folderPath = `public/models/${destination}/${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 // Classify each file: changed, new, or unchanged const fileChanges = new Map() const changedFilesToPush: { path: string; contentBase64: string }[] = [] for (const f of filesToPush) { const filename = f.path.split('/').pop() ?? '' const localSize = Buffer.from(f.contentBase64, 'base64').length const remoteSize = remoteFileMap.get(filename.toLowerCase()) if (remoteSize === undefined) { fileChanges.set(filename.toLowerCase(), 'new') changedFilesToPush.push(f) } else if (remoteSize !== localSize) { fileChanges.set(filename.toLowerCase(), 'changed') changedFilesToPush.push(f) } else { fileChanges.set(filename.toLowerCase(), 'unchanged') } } // Files on remote not in the new upload → deleted (orphans) const newFileNames = new Set(filesToPush.map((f) => (f.path.split('/').pop() ?? '').toLowerCase())) const deletedFileNames: string[] = [] const deletePaths: string[] = [] for (const [name] of remoteFileMap) { if (!newFileNames.has(name)) { deletedFileNames.push(name) deletePaths.push(`${folderPath}/${name}`) } } // 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, destination, 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' return NextResponse.json( { success: false, error: `Push GitHub echoue: ${message}` }, { status: 500 }, ) } }