3adcf9d30e
Models are always re-pushed server-side since Draco compression changes the file size, making client/remote size comparison unreliable. Textures are still compared by size (not compressed, reliable). Client-side diff now only flags models as 'new' if absent from remote, and never as 'changed' (server handles the actual push decision).
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
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, MODEL_EXTENSIONS } 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<ReturnType<typeof parseMultiUpload>>['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 classify changes ---
|
|
// Models: always re-push (compression changes size, can't compare with LFS remote)
|
|
// Textures: compare by size (not compressed, reliable)
|
|
const folderPath = `public/models/${destination}/${folderName}`
|
|
let remoteFileMap: Map<string, number>
|
|
|
|
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 = new Map<string, FileChange>()
|
|
const changedFilesToPush: { path: string; contentBase64: string }[] = []
|
|
|
|
for (const f of filesToPush) {
|
|
const filename = f.path.split('/').pop() ?? ''
|
|
const ext = filename.slice(filename.lastIndexOf('.')).toLowerCase()
|
|
const isModel = MODEL_EXTENSIONS.has(ext)
|
|
|
|
if (isModel) {
|
|
// Model: always re-push since compression makes size comparison unreliable
|
|
const remoteSize = remoteFileMap.get(filename.toLowerCase())
|
|
fileChanges.set(filename.toLowerCase(), remoteSize === undefined ? 'new' : 'changed')
|
|
changedFilesToPush.push(f)
|
|
} else {
|
|
// Texture: compare by size
|
|
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 },
|
|
)
|
|
}
|
|
}
|