import { NextRequest, NextResponse } from 'next/server' import { Octokit } from '@octokit/rest' import { extname, basename } from 'path' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' const MODEL_EXTENSIONS = new Set(['.glb', '.gltf']) const TEXTURE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']) const ALL_ALLOWED_EXTENSIONS = new Set([...MODEL_EXTENSIONS, ...TEXTURE_EXTENSIONS]) function sanitizeFilename(name: string): string { return basename(name) .replace(/[^a-zA-Z0-9._-]/g, '_') .replace(/_{2,}/g, '_') .toLowerCase() } function getOctokit(): Octokit { const token = process.env.GITHUB_TOKEN if (!token) throw new Error('GITHUB_TOKEN non configuré') return new Octokit({ auth: token }) } function parseRepoUrl(): { owner: string; repo: string } { const url = process.env.GIT_REPO_URL if (!url) throw new Error('GIT_REPO_URL non configuré') // Support formats: https://github.com/owner/repo.git, owner/repo, git@github.com:owner/repo.git const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/) const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/) const shortMatch = url.match(/^([^/]+)\/([^/]+)$/) const match = httpsMatch || sshMatch || shortMatch if (!match) throw new Error(`Format GIT_REPO_URL invalide: "${url}"`) return { owner: match[1], repo: match[2] } } async function parseUpload(req: NextRequest) { const formData = await req.formData() const file = formData.get('file') as File | null const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets' const fileType = formData.get('fileType') as string | null const textureName = formData.get('textureName') as string | null if (!file || file.size === 0) { throw new Error('Aucun fichier reçu') } const originalSafe = sanitizeFilename(file.name) const ext = extname(originalSafe).toLowerCase() if (!ALL_ALLOWED_EXTENSIONS.has(ext)) { throw new Error(`Extension non autorisée: "${ext}"`) } const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-') let filename: string if (fileType === 'texture' && textureName) { filename = sanitizeFilename(textureName) } else { filename = originalSafe } const buffer = Buffer.from(await file.arrayBuffer()) const content = buffer.toString('base64') const path = `public/assets/${safeFolderName}/${filename}` return { filename, content, path, folderName: safeFolderName } } async function pushToGitHub( filePath: string, content: string, folderName: string ): Promise<{ commitUrl: string }> { const octokit = getOctokit() const { owner, repo } = parseRepoUrl() const branch = process.env.GIT_BRANCH ?? 'main' // 1. Get the current commit SHA on the branch const { data: ref } = await octokit.git.getRef({ owner, repo, ref: `heads/${branch}`, }) const latestCommitSha = ref.object.sha // 2. Get the tree of the latest commit const { data: commit } = await octokit.git.getCommit({ owner, repo, commit_sha: latestCommitSha, }) // 3. Create a blob for the file const { data: blob } = await octokit.git.createBlob({ owner, repo, content, encoding: 'base64', }) // 4. Create a new tree with the file added const { data: newTree } = await octokit.git.createTree({ owner, repo, base_tree: commit.tree.sha, tree: [ { path: filePath, mode: '100644', type: 'blob', sha: blob.sha, }, ], }) // 5. Create a new commit const timestamp = new Date().toISOString() const { data: newCommit } = await octokit.git.createCommit({ owner, repo, message: `Design Update: ${folderName} [${timestamp}]`, tree: newTree.sha, parents: [latestCommitSha], }) // 6. Update the branch reference await octokit.git.updateRef({ owner, repo, ref: `heads/${branch}`, sha: newCommit.sha, }) return { commitUrl: newCommit.html_url } } export async function POST(req: NextRequest) { const secret = req.headers.get('x-upload-secret') const expectedSecret = process.env.UPLOAD_SECRET_KEY if (!expectedSecret) { return NextResponse.json( { success: false, error: 'Configuration serveur incomplète (UPLOAD_SECRET_KEY manquant)' }, { status: 500 } ) } if (!secret || secret !== expectedSecret) { return NextResponse.json( { success: false, error: "Clé d'authentification invalide" }, { status: 401 } ) } let filename: string let content: string let path: string let folderName: string try { ;({ filename, content, path, folderName } = await parseUpload(req)) } catch (err) { const message = err instanceof Error ? err.message : 'Erreur inconnue' return NextResponse.json({ success: false, error: message }, { status: 400 }) } try { const { commitUrl } = await pushToGitHub(path, content, folderName) return NextResponse.json({ success: true, filename, message: `"${filename}" ajouté au dossier "${folderName}" et poussé sur GitHub.`, commitUrl, }) } catch (err) { const message = err instanceof Error ? err.message : 'Erreur GitHub inconnue' return NextResponse.json( { success: false, error: `Push GitHub échoué: ${message}` }, { status: 500 } ) } }