refactor: simplify upload rules and remove destination flow
This commit is contained in:
+9
-23
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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: [] }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user