refactor: simplify upload rules and remove destination flow

This commit is contained in:
Tom Boullay
2026-04-24 16:23:02 +02:00
parent 61a0146545
commit 944959fc22
20 changed files with 2033 additions and 217 deletions
+9 -23
View File
@@ -1,4 +1,3 @@
import { REQUIRED_TEXTURES } from './constants'
import type { FileChange } from './types'
/**
@@ -7,12 +6,11 @@ import type { FileChange } from './types'
* Symbols:
* - ✅ = new file
* - 🔄 = modified file
* - ❌ = missing texture (new upload) or deleted file (update)
* - ❌ = deleted file
* - Unchanged files are omitted entirely
*/
export function buildCommitMessage(
folderName: string,
destination: string,
modelFilename: string,
textureNames: string[],
compressed: boolean,
@@ -21,8 +19,8 @@ export function buildCommitMessage(
deletedFileNames: string[],
): string {
const title = isReplace
? `update: upload-gltf update -> ${destination}/${folderName}`
: `update: upload-gltf add a new model -> ${destination}/${folderName}`
? `update: upload-gltf update -> ${folderName}`
: `update: upload-gltf add a new model -> ${folderName}`
const lines: string[] = [title, '']
@@ -39,26 +37,14 @@ export function buildCommitMessage(
lines.push(` ↔️ ${modelFilename} (inchange)`)
}
// Textures section — only show lines that have changes
const foundTextures = new Set(
textureNames.map((t) => t.toLowerCase().replace(/\.[^.]+$/, '')),
)
const textureLines: string[] = []
for (const tex of REQUIRED_TEXTURES) {
if (foundTextures.has(tex)) {
const actual = textureNames.find(
(t) => t.toLowerCase().replace(/\.[^.]+$/, '') === tex,
)!
const change = fileChanges.get(actual.toLowerCase())
if (change === 'new') {
textureLines.push(`${actual}`)
} else if (change === 'changed') {
textureLines.push(` 🔄 ${actual}`)
}
} else if (!isReplace) {
textureLines.push(`${tex} (manquant)`)
for (const textureName of textureNames) {
const change = fileChanges.get(textureName.toLowerCase())
if (change === 'new') {
textureLines.push(`${textureName}`)
} else if (change === 'changed') {
textureLines.push(` 🔄 ${textureName}`)
}
}
-17
View File
@@ -9,23 +9,6 @@ export const ALL_ALLOWED_EXTENSIONS = new Set([...MODEL_EXTENSIONS, ...TEXTURE_E
/** Extensions tracked by Git LFS (must match .gitattributes) */
export const LFS_EXTENSIONS = new Set(['.glb', '.gltf', '.png', '.jpg', '.jpeg', '.webp'])
export const REQUIRED_TEXTURES = ['roughness', 'normal', 'metalness', 'color', 'displace'] as const
export const VALID_DESTINATIONS = new Set<string>([
'farm', 'map', 'powergrid', 'workshop', 'general', 'environment',
])
export const DESTINATIONS = [
{ value: 'farm', label: 'Farm' },
{ value: 'map', label: 'Map' },
{ value: 'powergrid', label: 'Powergrid' },
{ value: 'workshop', label: 'Workshop' },
{ value: 'general', label: 'General' },
{ value: 'environment', label: 'Environment' },
] as const
export type Destination = typeof DESTINATIONS[number]['value']
export const TMP_DIR = '/tmp/assets'
/** Maximum file size in bytes (100 MB) */
+1 -1
View File
@@ -51,7 +51,7 @@ async function davRequest(
Authorization: auth,
...extraHeaders,
},
body: body ?? undefined,
body: body == null ? undefined : typeof body === 'string' ? body : new Uint8Array(body),
})
return res
+5 -12
View File
@@ -1,12 +1,11 @@
import { extname } from 'path'
import { NextRequest } from 'next/server'
import { sanitizeFilename } from './sanitize'
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, VALID_DESTINATIONS, MAX_FILE_SIZE } from './constants'
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE } from './constants'
import type { ParsedFile } from './types'
export interface ParsedUpload {
folderName: string
destination: string
files: ParsedFile[]
/** Any extra string fields from the FormData (e.g. "action") */
extra: Record<string, string>
@@ -14,8 +13,8 @@ export interface ParsedUpload {
/**
* Parse a multi-file FormData upload request.
* Validates destination, file extensions, file sizes, and returns parsed files.
* Extra string fields (beyond folderName, destination, files, fileTypes, textureNames)
* Validates file extensions, file sizes, and returns parsed files.
* Extra string fields (beyond folderName, files, fileTypes, textureNames)
* are returned in `extra`.
*/
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
@@ -23,18 +22,12 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets'
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
const rawDestination = (formData.get('destination') as string | null)?.trim() || 'general'
if (!VALID_DESTINATIONS.has(rawDestination)) {
throw new Error(`Destination invalide: "${rawDestination}"`)
}
const destination = rawDestination
const rawFiles = formData.getAll('files')
const fileTypes = formData.getAll('fileTypes') as string[]
const textureNames = formData.getAll('textureNames') as string[]
// Collect extra string fields
const knownKeys = new Set(['folderName', 'destination', 'files', 'fileTypes', 'textureNames'])
const knownKeys = new Set(['folderName', 'files', 'fileTypes', 'textureNames'])
const extra: Record<string, string> = {}
for (const [key, value] of formData.entries()) {
if (!knownKeys.has(key) && typeof value === 'string') {
@@ -91,5 +84,5 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
parsed.push({ filename, buffer, isModel })
}
return { folderName: safeFolderName, destination, files: parsed, extra }
return { folderName: safeFolderName, files: parsed, extra }
}
+1 -3
View File
@@ -13,7 +13,6 @@ interface PushFile {
interface PrepareGitAssetsParams {
folderName: string
destination: string
parsedFiles: ParsedFile[]
}
@@ -27,7 +26,6 @@ interface PrepareGitAssetsResult {
export async function prepareGitAssets({
folderName,
destination,
parsedFiles,
}: PrepareGitAssetsParams): Promise<PrepareGitAssetsResult> {
const filesToPush: PushFile[] = []
@@ -76,7 +74,7 @@ export async function prepareGitAssets({
}
filesToPush.push({
path: `public/models/${destination}/${folderName}/${pf.filename}`,
path: `public/models/${folderName}/${pf.filename}`,
contentBase64: content.toString('base64'),
})
}
+3 -8
View File
@@ -16,12 +16,10 @@ export interface CheckResult {
function buildUploadFormData(
folder: FolderEntry,
destination: string,
extra?: Record<string, string>,
): FormData {
const formData = new FormData()
formData.append('folderName', folder.folderName)
formData.append('destination', destination)
if (extra) {
for (const [key, value] of Object.entries(extra)) {
@@ -52,11 +50,10 @@ function buildUploadFormData(
*/
export async function checkFolderDiffs(
folder: FolderEntry,
destination: string,
secret: string,
signal?: AbortSignal,
): Promise<CheckResult> {
const formData = buildUploadFormData(folder, destination)
const formData = buildUploadFormData(folder)
const res = await fetch('/api/upload/check', {
method: 'POST',
headers: { 'x-upload-secret': secret.trim() },
@@ -86,11 +83,10 @@ export async function checkFolderDiffs(
export async function uploadDrive(
folder: FolderEntry,
secret: string,
destination: string,
action: 'new' | 'replace',
signal?: AbortSignal,
): Promise<{ success: boolean; error?: string }> {
const formData = buildUploadFormData(folder, destination, { action })
const formData = buildUploadFormData(folder, { action })
try {
const res = await fetch('/api/upload/drive', {
@@ -118,11 +114,10 @@ export async function uploadDrive(
export async function uploadGit(
folder: FolderEntry,
secret: string,
destination: string,
onProgress: (pct: number) => void,
signal?: AbortSignal,
): Promise<{ success: boolean; filename?: string; error?: string }> {
const formData = buildUploadFormData(folder, destination)
const formData = buildUploadFormData(folder)
onProgress(10)
+9 -21
View File
@@ -2,17 +2,11 @@
// Client-side folder validation
// ---------------------------------------------------------------------------
import { REQUIRED_TEXTURES, TEXTURE_EXTENSIONS } from '@/lib/constants'
import { TEXTURE_EXTENSIONS } from '@/lib/constants'
import type { TextureFile } from '@/lib/client-types'
const TEXTURE_EXT_ARRAY = [...TEXTURE_EXTENSIONS]
function getTextureType(filename: string): string | null {
const name = filename.toLowerCase().replace(/\.[^.]+$/, '')
if ((REQUIRED_TEXTURES as readonly string[]).includes(name)) return name
return null
}
/** Discriminated union: either valid (with model) or invalid (with errors). */
export type ValidationResult =
| { ok: true; model: File; textures: TextureFile[]; warnings: string[] }
@@ -20,39 +14,33 @@ export type ValidationResult =
export function validateFolder(files: File[]): ValidationResult {
const textures: TextureFile[] = []
const warnings: string[] = []
const errors: string[] = []
const modelFiles = files.filter((f) => {
const name = f.name.toLowerCase()
return name === 'model.glb' || name === 'model.gltf'
return name === 'model.glb'
})
if (modelFiles.length === 0) {
return { ok: false, errors: ['model.glb ou model.gltf manquant (obligatoire)'] }
return { ok: false, errors: ['model.glb manquant (obligatoire)'] }
}
if (modelFiles.length > 1) {
return { ok: false, errors: ['Un seul fichier model.glb est autorise'] }
}
const textureFiles = files.filter((f) => {
const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase()
return TEXTURE_EXT_ARRAY.includes(ext) && getTextureType(f.name) !== null
return TEXTURE_EXT_ARRAY.includes(ext)
})
for (const tf of textureFiles) {
textures.push({ name: tf.name, file: tf })
}
const foundTextures = new Set(
textures.map((t) => t.name.toLowerCase().replace(/\.[^.]+$/, '')),
)
for (const req of REQUIRED_TEXTURES) {
if (!foundTextures.has(req)) {
warnings.push(`${req}.webp/png/jpg manquant`)
}
}
if (errors.length > 0) {
return { ok: false, errors }
}
return { ok: true, model: modelFiles[0], textures, warnings }
return { ok: true, model: modelFiles[0], textures, warnings: [] }
}