fix: one commit
This commit is contained in:
+158
-89
@@ -14,6 +14,7 @@ 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])
|
||||
const REQUIRED_TEXTURES = ['roughness', 'normal', 'metalness', 'color', 'displace']
|
||||
|
||||
const TMP_DIR = join('/tmp', 'assets')
|
||||
|
||||
@@ -83,50 +84,109 @@ async function compressWithBlender(
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse uploaded file from FormData
|
||||
// Build commit message with checklist
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
function buildCommitMessage(
|
||||
folderName: string,
|
||||
modelFilename: string,
|
||||
textureNames: string[],
|
||||
compressed: boolean,
|
||||
): string {
|
||||
const title = `update: from upload-gltf add a new model -> ${folderName}`
|
||||
|
||||
if (!file || file.size === 0) {
|
||||
throw new Error('Aucun fichier recu')
|
||||
}
|
||||
const foundTextures = new Set(
|
||||
textureNames.map(t => t.toLowerCase().replace(/\.[^.]+$/, ''))
|
||||
)
|
||||
|
||||
const originalSafe = sanitizeFilename(file.name)
|
||||
const ext = extname(originalSafe).toLowerCase()
|
||||
const textureLines = REQUIRED_TEXTURES.map(tex => {
|
||||
if (foundTextures.has(tex)) {
|
||||
const actual = textureNames.find(t => t.toLowerCase().replace(/\.[^.]+$/, '') === tex)
|
||||
return ` ✅ ${actual}`
|
||||
}
|
||||
return ` ❌ ${tex} (manquant)`
|
||||
})
|
||||
|
||||
if (!ALL_ALLOWED_EXTENSIONS.has(ext)) {
|
||||
throw new Error(`Extension non autorisee: "${ext}"`)
|
||||
}
|
||||
const lines = [
|
||||
title,
|
||||
'',
|
||||
'📦 Model',
|
||||
` ✅ ${modelFilename}${compressed ? ' (Draco)' : ''}`,
|
||||
'',
|
||||
'🎨 Textures',
|
||||
...textureLines,
|
||||
]
|
||||
|
||||
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
|
||||
|
||||
let filename: string
|
||||
if (fileType === 'texture' && textureName) {
|
||||
filename = sanitizeFilename(textureName)
|
||||
} else {
|
||||
filename = originalSafe
|
||||
}
|
||||
|
||||
const isModel = MODEL_EXTENSIONS.has(ext)
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
return { filename, buffer, safeFolderName, isModel }
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push a single file to GitHub via the API
|
||||
// Parse all files from a multi-file FormData
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function pushFileToGitHub(
|
||||
filePath: string,
|
||||
contentBase64: string,
|
||||
interface ParsedFile {
|
||||
filename: string
|
||||
buffer: Buffer
|
||||
isModel: boolean
|
||||
textureName?: string
|
||||
}
|
||||
|
||||
async function parseMultiUpload(req: NextRequest): Promise<{
|
||||
folderName: string
|
||||
files: ParsedFile[]
|
||||
}> {
|
||||
const formData = await req.formData()
|
||||
const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets'
|
||||
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
|
||||
|
||||
const fileEntries = formData.getAll('files') as File[]
|
||||
const fileTypes = formData.getAll('fileTypes') as string[]
|
||||
const textureNames = formData.getAll('textureNames') as string[]
|
||||
|
||||
if (fileEntries.length === 0) {
|
||||
throw new Error('Aucun fichier recu')
|
||||
}
|
||||
|
||||
const parsed: ParsedFile[] = []
|
||||
|
||||
for (let i = 0; i < fileEntries.length; i++) {
|
||||
const file = fileEntries[i]
|
||||
if (!file || file.size === 0) continue
|
||||
|
||||
const fileType = fileTypes[i] || 'model'
|
||||
const texName = textureNames[i] || ''
|
||||
|
||||
const originalSafe = sanitizeFilename(file.name)
|
||||
const ext = extname(originalSafe).toLowerCase()
|
||||
|
||||
if (!ALL_ALLOWED_EXTENSIONS.has(ext)) {
|
||||
throw new Error(`Extension non autorisee: "${ext}"`)
|
||||
}
|
||||
|
||||
let filename: string
|
||||
if (fileType === 'texture' && texName) {
|
||||
filename = sanitizeFilename(texName)
|
||||
} else {
|
||||
filename = originalSafe
|
||||
}
|
||||
|
||||
const isModel = MODEL_EXTENSIONS.has(ext)
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
parsed.push({ filename, buffer, isModel, textureName: texName || undefined })
|
||||
}
|
||||
|
||||
return { folderName: safeFolderName, files: parsed }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push all files in a single commit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function pushAllToGitHub(
|
||||
folderName: string,
|
||||
files: { path: string; contentBase64: string }[],
|
||||
commitMessage: string
|
||||
): Promise<{ commitUrl: string }> {
|
||||
const octokit = getOctokit()
|
||||
const { owner, repo } = parseRepoUrl()
|
||||
@@ -147,35 +207,36 @@ async function pushFileToGitHub(
|
||||
commit_sha: latestCommitSha,
|
||||
})
|
||||
|
||||
// 3. Create blob
|
||||
const { data: blob } = await octokit.git.createBlob({
|
||||
owner,
|
||||
repo,
|
||||
content: contentBase64,
|
||||
encoding: 'base64',
|
||||
})
|
||||
// 3. Create all blobs in parallel
|
||||
const blobResults = await Promise.all(
|
||||
files.map(f =>
|
||||
octokit.git.createBlob({
|
||||
owner,
|
||||
repo,
|
||||
content: f.contentBase64,
|
||||
encoding: 'base64',
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// 4. Create tree
|
||||
// 4. Create a single tree with all files
|
||||
const { data: newTree } = await octokit.git.createTree({
|
||||
owner,
|
||||
repo,
|
||||
base_tree: commit.tree.sha,
|
||||
tree: [
|
||||
{
|
||||
path: filePath,
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: blob.sha,
|
||||
},
|
||||
],
|
||||
tree: files.map((f, i) => ({
|
||||
path: f.path,
|
||||
mode: '100644' as const,
|
||||
type: 'blob' as const,
|
||||
sha: blobResults[i].data.sha,
|
||||
})),
|
||||
})
|
||||
|
||||
// 5. Create commit
|
||||
const timestamp = new Date().toISOString()
|
||||
// 5. Create a single commit
|
||||
const { data: newCommit } = await octokit.git.createCommit({
|
||||
owner,
|
||||
repo,
|
||||
message: `Design Update: ${folderName} [${timestamp}]`,
|
||||
message: commitMessage,
|
||||
tree: newTree.sha,
|
||||
parents: [latestCommitSha],
|
||||
})
|
||||
@@ -214,67 +275,75 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// --- Parse upload ---
|
||||
let filename: string
|
||||
let buffer: Buffer
|
||||
let safeFolderName: string
|
||||
let isModel: boolean
|
||||
// --- Parse all files ---
|
||||
let folderName: string
|
||||
let parsedFiles: ParsedFile[]
|
||||
|
||||
try {
|
||||
;({ filename, buffer, safeFolderName, isModel } = await parseUpload(req))
|
||||
;({ folderName, files: parsedFiles } = await parseMultiUpload(req))
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
}
|
||||
|
||||
// --- Write to /tmp ---
|
||||
const tmpFolder = join(TMP_DIR, safeFolderName)
|
||||
await mkdir(tmpFolder, { recursive: true })
|
||||
const tmpFilePath = join(tmpFolder, filename)
|
||||
await writeFile(tmpFilePath, buffer)
|
||||
|
||||
let contentToUpload: Buffer = buffer
|
||||
// --- Process files (compress model if possible) ---
|
||||
const filesToPush: { path: string; contentBase64: string }[] = []
|
||||
let modelFilename = ''
|
||||
let compressed = false
|
||||
let compressionError: string | undefined
|
||||
const textureNames: string[] = []
|
||||
|
||||
// --- Compress model with Blender (if available) ---
|
||||
if (isModel) {
|
||||
const stem = filename.replace(/\.[^.]+$/, '')
|
||||
const compressedPath = join(tmpFolder, `${stem}_compressed.glb`)
|
||||
for (const pf of parsedFiles) {
|
||||
let content = pf.buffer
|
||||
|
||||
const result = await compressWithBlender(tmpFilePath, compressedPath)
|
||||
if (pf.isModel) {
|
||||
modelFilename = pf.filename
|
||||
|
||||
if (result.success && existsSync(compressedPath)) {
|
||||
contentToUpload = await readFile(compressedPath)
|
||||
compressed = true
|
||||
// Clean up compressed temp file
|
||||
await unlink(compressedPath).catch(() => {})
|
||||
// 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`)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
await unlink(tmpFilePath).catch(() => {})
|
||||
await rm(tmpFolder, { recursive: true, force: true }).catch(() => {})
|
||||
} else {
|
||||
// Blender not available or failed — push original
|
||||
compressionError = result.error
|
||||
textureNames.push(pf.filename)
|
||||
}
|
||||
|
||||
filesToPush.push({
|
||||
path: `public/assets/${folderName}/${pf.filename}`,
|
||||
contentBase64: content.toString('base64'),
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up original temp file
|
||||
await unlink(tmpFilePath).catch(() => {})
|
||||
// Clean up folder if empty
|
||||
await rm(tmpFolder, { recursive: true, force: true }).catch(() => {})
|
||||
|
||||
// --- Push to GitHub ---
|
||||
const gitPath = `public/assets/${safeFolderName}/${filename}`
|
||||
const contentBase64 = contentToUpload.toString('base64')
|
||||
// --- Build commit message ---
|
||||
const commitMessage = buildCommitMessage(folderName, modelFilename, textureNames, compressed)
|
||||
|
||||
// --- Push all in one commit ---
|
||||
try {
|
||||
const { commitUrl } = await pushFileToGitHub(gitPath, contentBase64, safeFolderName)
|
||||
const { commitUrl } = await pushAllToGitHub(folderName, filesToPush, commitMessage)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
filename,
|
||||
folderName,
|
||||
filesCount: filesToPush.length,
|
||||
compressed,
|
||||
compressionError: compressionError || undefined,
|
||||
message: compressed
|
||||
? `"${filename}" compresse avec Draco et pousse sur GitHub.`
|
||||
: `"${filename}" pousse sur GitHub${compressionError ? ' (sans compression: ' + compressionError + ')' : ''}.`,
|
||||
message: `${filesToPush.length} fichier(s) envoye(s) sur GitHub en un seul commit.`,
|
||||
commitUrl,
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: ' Upload GLTF',
|
||||
description: 'Interface de dépôt sécurisé pour fichiers 3D (.glb, .gltf, .fbx) et de versionnement automatique',
|
||||
description: 'Interface de depot securise pour fichiers 3D (.glb, .gltf) avec versionnement automatique sur GitHub',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
+4
-6
@@ -8,19 +8,17 @@ export default function Home() {
|
||||
Upload GLTF
|
||||
</h1>
|
||||
<p className="text-gray-400 text-base leading-relaxed">
|
||||
Drop your 3D files — they will be automatically versioned
|
||||
<br />and pushed to your GitHub repository via Git LFS.
|
||||
Deposez vos fichiers 3D — ils seront automatiquement versionnes
|
||||
<br />et envoyes sur votre depot GitHub.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UploadZone />
|
||||
|
||||
<footer className="mt-10 text-gray-500 text-xs text-center">
|
||||
Models: <span className="font-mono text-gray-400">.glb · .gltf</span>
|
||||
Modeles : <span className="font-mono text-gray-400">.glb · .gltf</span>
|
||||
<span className="mx-2">·</span>
|
||||
Textures: <span className="font-mono text-gray-400">.png · .jpg · .webp</span>
|
||||
<span className="mx-2">·</span>
|
||||
Max size: <span className="font-mono text-gray-400">2 GB</span>
|
||||
Textures : <span className="font-mono text-gray-400">.png · .jpg · .webp</span>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user