91eaa5d186
Git LFS stores pointer files whose SHA differs from the actual blob SHA, causing false-positive diffs on every upload. Switching to file size comparison resolves this for LFS-enabled repos. Also replaces the inline error message with a dedicated NoChangesModal when no differences are detected, offering cancel (reset) or modify (close modal) actions.
170 lines
5.6 KiB
TypeScript
170 lines
5.6 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 } 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 compare size to classify changes (LFS-compatible) ---
|
|
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
|
|
|
|
// Classify each file: changed, new, or unchanged
|
|
const fileChanges = new Map<string, FileChange>()
|
|
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 },
|
|
)
|
|
}
|
|
}
|