fix: validate texture asset names server-side
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import { getAssetFamily } from './asset-naming'
|
||||
|
||||
export type AssetCategory = 'color' | 'roughness' | 'normal' | 'metalness' | 'assets'
|
||||
export type AssetCategory = 'color' | 'diffuse' | 'roughness' | 'normal' | 'metalness' | 'opacity' | 'assets'
|
||||
|
||||
export function classifyAssetCategory(filename: string): AssetCategory {
|
||||
const name = filename.replace(/\.[^.]+$/, '')
|
||||
const family = getAssetFamily(name.split('_')[0])
|
||||
|
||||
if (family === 'color' || family === 'diffuse') {
|
||||
if (family === 'color') {
|
||||
return 'color'
|
||||
}
|
||||
|
||||
if (family === 'diffuse') {
|
||||
return 'diffuse'
|
||||
}
|
||||
|
||||
if (family === 'roughness') {
|
||||
return 'roughness'
|
||||
}
|
||||
@@ -22,5 +26,9 @@ export function classifyAssetCategory(filename: string): AssetCategory {
|
||||
return 'metalness'
|
||||
}
|
||||
|
||||
if (family === 'opacity') {
|
||||
return 'opacity'
|
||||
}
|
||||
|
||||
return 'assets'
|
||||
}
|
||||
|
||||
@@ -29,6 +29,44 @@ export function getForbiddenAssetFamilyAlias(value: string): AssetFamily | undef
|
||||
return FORBIDDEN_ASSET_FAMILY_ALIASES.get(value.toLowerCase())
|
||||
}
|
||||
|
||||
function getFileStem(filename: string) {
|
||||
return filename.replace(/\.[^.]+$/, '')
|
||||
}
|
||||
|
||||
export function getTextureNamingError(filename: string) {
|
||||
const stem = getFileStem(filename)
|
||||
const [prefix, ...targetParts] = stem.split('_')
|
||||
const family = getAssetFamily(prefix)
|
||||
const extension = filename.split('.').pop()
|
||||
|
||||
if (family && targetParts.every(Boolean)) return null
|
||||
|
||||
const aliasSuggestion = getForbiddenAssetFamilyAlias(prefix)
|
||||
|
||||
if (aliasSuggestion && targetParts.every(Boolean)) {
|
||||
const target = targetParts.join('_')
|
||||
return `Convention invalide : ${filename}. Utilisez ${aliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${aliasSuggestion}.${extension} pour tout le modele.`
|
||||
}
|
||||
|
||||
const reversedParts = stem.split('_')
|
||||
const reversedFamily = reversedParts.length > 1 ? getAssetFamily(reversedParts[reversedParts.length - 1]) : undefined
|
||||
const reversedAliasSuggestion = reversedParts.length > 1
|
||||
? getForbiddenAssetFamilyAlias(reversedParts[reversedParts.length - 1])
|
||||
: undefined
|
||||
|
||||
if (reversedFamily) {
|
||||
const target = reversedParts.slice(0, -1).join('_')
|
||||
return `Convention invalide : ${filename}. Utilisez ${reversedFamily}_${target}.${extension} pour cibler "${target}", ou ${reversedFamily}.${extension} pour tout le modele.`
|
||||
}
|
||||
|
||||
if (reversedAliasSuggestion) {
|
||||
const target = reversedParts.slice(0, -1).join('_')
|
||||
return `Convention invalide : ${filename}. Utilisez ${reversedAliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${reversedAliasSuggestion}.${extension} pour tout le modele.`
|
||||
}
|
||||
|
||||
return `Asset inconnu : ${filename}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.`
|
||||
}
|
||||
|
||||
export function formatAssetFamilies() {
|
||||
return ASSET_FAMILIES.join(', ')
|
||||
}
|
||||
|
||||
@@ -58,13 +58,15 @@ export function buildCommitMessage(
|
||||
|
||||
const sectionTitles: Record<AssetCategory, string> = {
|
||||
color: '🎨 Textures (color)',
|
||||
diffuse: '🖌 Textures (diffuse)',
|
||||
roughness: '🪶 Textures (roughness)',
|
||||
normal: '🧭 Textures (normal)',
|
||||
metalness: '🔩 Textures (metalness)',
|
||||
opacity: '🪟 Textures (opacity)',
|
||||
assets: '🧩 Assets',
|
||||
}
|
||||
|
||||
for (const category of ['color', 'roughness', 'normal', 'metalness', 'assets'] as const) {
|
||||
for (const category of ['color', 'diffuse', 'roughness', 'normal', 'metalness', 'opacity', 'assets'] as const) {
|
||||
const entries = grouped.get(category)
|
||||
if (!entries || entries.length === 0) continue
|
||||
lines.push('')
|
||||
|
||||
+19
-5
@@ -1,7 +1,8 @@
|
||||
import { extname } from 'path'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { sanitizeFilename } from './sanitize'
|
||||
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE } from './constants'
|
||||
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE, TEXTURE_EXTENSIONS } from './constants'
|
||||
import { getTextureNamingError } from './asset-naming'
|
||||
import type { ParsedFile } from './types'
|
||||
|
||||
interface ParsedUpload {
|
||||
@@ -48,10 +49,10 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
||||
const texName = textureNames[i] || ''
|
||||
|
||||
const originalSafe = sanitizeFilename(file.name)
|
||||
const ext = extname(originalSafe).toLowerCase()
|
||||
const originalExt = extname(originalSafe).toLowerCase()
|
||||
|
||||
if (!ALL_ALLOWED_EXTENSIONS.has(ext)) {
|
||||
throw new Error(`Extension non autorisee: "${ext}"`)
|
||||
if (!ALL_ALLOWED_EXTENSIONS.has(originalExt)) {
|
||||
throw new Error(`Extension non autorisee: "${originalExt}"`)
|
||||
}
|
||||
|
||||
let filename: string
|
||||
@@ -61,7 +62,20 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
||||
filename = originalSafe
|
||||
}
|
||||
|
||||
const isModel = MODEL_EXTENSIONS.has(ext)
|
||||
const filenameExt = extname(filename).toLowerCase()
|
||||
if (filenameExt !== originalExt) {
|
||||
throw new Error(`Nom de fichier incoherent : ${filename} ne correspond pas a l'extension originale ${originalExt}`)
|
||||
}
|
||||
|
||||
const textureNamingError = TEXTURE_EXTENSIONS.has(filenameExt)
|
||||
? getTextureNamingError(filename)
|
||||
: null
|
||||
|
||||
if (textureNamingError) {
|
||||
throw new Error(textureNamingError)
|
||||
}
|
||||
|
||||
const isModel = MODEL_EXTENSIONS.has(filenameExt)
|
||||
if (isModel) {
|
||||
if (filename.toLowerCase() !== 'model.gltf') {
|
||||
throw new Error('Le modele doit etre nomme model.gltf')
|
||||
|
||||
+2
-40
@@ -1,5 +1,5 @@
|
||||
import { ASSET_EXTENSIONS, TEXTURE_EXTENSIONS } from '@/lib/constants'
|
||||
import { formatAssetFamilies, getAssetFamily, getForbiddenAssetFamilyAlias } from '@/lib/asset-naming'
|
||||
import { getTextureNamingError } from '@/lib/asset-naming'
|
||||
import { getErrorMessage, isRecord } from '@/lib/guards'
|
||||
import type { TextureFile } from '@/lib/client-types'
|
||||
|
||||
@@ -37,44 +37,6 @@ function getFileExtension(filename: string) {
|
||||
return filename.slice(filename.lastIndexOf('.')).toLowerCase()
|
||||
}
|
||||
|
||||
function getFileStem(filename: string) {
|
||||
return filename.replace(/\.[^.]+$/, '')
|
||||
}
|
||||
|
||||
function getTextureNamingError(file: File) {
|
||||
const stem = getFileStem(file.name)
|
||||
const [prefix, ...targetParts] = stem.split('_')
|
||||
const family = getAssetFamily(prefix)
|
||||
const extension = file.name.split('.').pop()
|
||||
|
||||
if (family && targetParts.every(Boolean)) return null
|
||||
|
||||
const aliasSuggestion = getForbiddenAssetFamilyAlias(prefix)
|
||||
|
||||
if (aliasSuggestion && targetParts.every(Boolean)) {
|
||||
const target = targetParts.join('_')
|
||||
return `Convention invalide : ${file.name}. Utilisez ${aliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${aliasSuggestion}.${extension} pour tout le modele.`
|
||||
}
|
||||
|
||||
const reversedParts = stem.split('_')
|
||||
const reversedFamily = reversedParts.length > 1 ? getAssetFamily(reversedParts[reversedParts.length - 1]) : undefined
|
||||
const reversedAliasSuggestion = reversedParts.length > 1
|
||||
? getForbiddenAssetFamilyAlias(reversedParts[reversedParts.length - 1])
|
||||
: undefined
|
||||
|
||||
if (reversedFamily) {
|
||||
const target = reversedParts.slice(0, -1).join('_')
|
||||
return `Convention invalide : ${file.name}. Utilisez ${reversedFamily}_${target}.${extension} pour cibler "${target}", ou ${reversedFamily}.${extension} pour tout le modele.`
|
||||
}
|
||||
|
||||
if (reversedAliasSuggestion) {
|
||||
const target = reversedParts.slice(0, -1).join('_')
|
||||
return `Convention invalide : ${file.name}. Utilisez ${reversedAliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${reversedAliasSuggestion}.${extension} pour tout le modele.`
|
||||
}
|
||||
|
||||
return `Asset inconnu : ${file.name}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.`
|
||||
}
|
||||
|
||||
async function getGltfWarnings(model: File, supportFiles: File[]) {
|
||||
const warnings: string[] = []
|
||||
let parsed: unknown
|
||||
@@ -132,7 +94,7 @@ export async function validateFolder(files: File[]): Promise<ValidationResult> {
|
||||
|
||||
const textureNamingErrors = supportFiles
|
||||
.filter((file) => TEXTURE_EXTENSIONS.has(getFileExtension(file.name)))
|
||||
.map(getTextureNamingError)
|
||||
.map((file) => getTextureNamingError(file.name))
|
||||
.filter((error): error is string => Boolean(error))
|
||||
|
||||
errors.push(...textureNamingErrors)
|
||||
|
||||
Reference in New Issue
Block a user