diff --git a/app/api/upload/check/route.ts b/app/api/upload/check/route.ts index ca4332b..80c628f 100644 --- a/app/api/upload/check/route.ts +++ b/app/api/upload/check/route.ts @@ -4,6 +4,7 @@ import { getRemoteFolder } from '@/lib/github' import { classifyFileChanges } from '@/lib/diff-files' import { getModelFolderPath } from '@/lib/model-paths' import { ensurePreparedStagingAssets } from '@/lib/upload-staging' +import { parseStagingRequestBody } from '@/lib/upload-request' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -19,11 +20,8 @@ export async function POST(req: NextRequest) { let stagingId: string try { - const body = await req.json() - stagingId = body.stagingId - if (!stagingId || typeof stagingId !== 'string') { - throw new Error('stagingId manquant') - } + const body: unknown = await req.json() + stagingId = parseStagingRequestBody(body).stagingId } catch (err) { const message = err instanceof Error ? err.message : 'Erreur inconnue' return NextResponse.json({ success: false, error: message }, { status: 400 }) diff --git a/app/api/upload/drive/route.ts b/app/api/upload/drive/route.ts index 0c97865..738fbd4 100644 --- a/app/api/upload/drive/route.ts +++ b/app/api/upload/drive/route.ts @@ -8,6 +8,7 @@ import { findNextVersion, } from '@/lib/nextcloud' import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock' +import { parseDriveRequestBody } from '@/lib/upload-request' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -45,15 +46,13 @@ export async function POST(req: NextRequest) { // --- Parse staging request --- let folderName: string let parsedFiles: Awaited>['files'] - let action: string + let action: 'new' | 'replace' try { - const body = await req.json() - const stagingId = body.stagingId - if (!stagingId || typeof stagingId !== 'string') { - throw new Error('stagingId manquant') - } - action = typeof body.action === 'string' ? body.action.trim() || 'new' : 'new' + const body: unknown = await req.json() + const parsedBody = parseDriveRequestBody(body) + action = parsedBody.action + const stagingId = parsedBody.stagingId const staged = await readStagedOriginalFiles(stagingId) folderName = staged.folderName parsedFiles = staged.files diff --git a/app/api/upload/git/route.ts b/app/api/upload/git/route.ts index 5e01e30..1b4da51 100644 --- a/app/api/upload/git/route.ts +++ b/app/api/upload/git/route.ts @@ -6,6 +6,7 @@ import { classifyFileChanges } from '@/lib/diff-files' import { getModelFolderPath } from '@/lib/model-paths' import { cleanupStagingUpload, ensurePreparedStagingAssets, readStagedManifest } from '@/lib/upload-staging' import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock' +import { parseStagingRequestBody } from '@/lib/upload-request' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -23,11 +24,8 @@ export async function POST(req: NextRequest) { let stagingId: string try { - const body = await req.json() - stagingId = body.stagingId - if (!stagingId || typeof stagingId !== 'string') { - throw new Error('stagingId manquant') - } + const body: unknown = await req.json() + stagingId = parseStagingRequestBody(body).stagingId const manifest = await readStagedManifest(stagingId) folderName = manifest.folderName } catch (err) { diff --git a/components/upload/FolderDropzone.tsx b/components/upload/FolderDropzone.tsx index 8aaf76c..80db7bf 100644 --- a/components/upload/FolderDropzone.tsx +++ b/components/upload/FolderDropzone.tsx @@ -28,12 +28,22 @@ function readDirectoryEntries(reader: FileSystemDirectoryReader) { }) } +function isFileEntry(entry: FileSystemEntry): entry is FileSystemFileEntry { + return entry.isFile +} + +function isDirectoryEntry(entry: FileSystemEntry): entry is FileSystemDirectoryEntry { + return entry.isDirectory +} + async function collectEntryFiles(entry: FileSystemEntry): Promise { - if (entry.isFile) { - return [await readDroppedFile(entry as FileSystemFileEntry)] + if (isFileEntry(entry)) { + return [await readDroppedFile(entry)] } - const reader = (entry as FileSystemDirectoryEntry).createReader() + if (!isDirectoryEntry(entry)) return [] + + const reader = entry.createReader() const files: File[] = [] while (true) { @@ -109,7 +119,8 @@ export default function FolderDropzone({ } const handleDragLeave = (e: React.DragEvent) => { - if (!e.currentTarget.contains(e.relatedTarget as Node | null)) { + const relatedTarget = e.relatedTarget + if (!(relatedTarget instanceof Node) || !e.currentTarget.contains(relatedTarget)) { setIsDragActive(false) } } @@ -127,7 +138,7 @@ export default function FolderDropzone({ .map((item) => item.webkitGetAsEntry?.()) .filter((entry): entry is FileSystemEntry => entry !== null) - const directoryEntries = rootEntries.filter((entry) => entry.isDirectory) + const directoryEntries = rootEntries.filter(isDirectoryEntry) if (directoryEntries.length > 0) { if (directoryEntries.length > 1) { @@ -135,7 +146,7 @@ export default function FolderDropzone({ return } - const rootEntry = directoryEntries[0] as FileSystemDirectoryEntry + const rootEntry = directoryEntries[0] const files = await collectEntryFiles(rootEntry) processFiles(files, rootEntry.name) return diff --git a/lib/parse-upload.ts b/lib/parse-upload.ts index 6963eff..6c01170 100644 --- a/lib/parse-upload.ts +++ b/lib/parse-upload.ts @@ -19,12 +19,13 @@ export interface ParsedUpload { */ export async function parseMultiUpload(req: NextRequest): Promise { const formData = await req.formData() - const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets' + const folderValue = formData.get('folderName') + const folderName = typeof folderValue === 'string' ? folderValue.trim() || 'assets' : 'assets' const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-') const rawFiles = formData.getAll('files') - const fileTypes = formData.getAll('fileTypes') as string[] - const textureNames = formData.getAll('textureNames') as string[] + const fileTypes = formData.getAll('fileTypes').filter((value): value is string => typeof value === 'string') + const textureNames = formData.getAll('textureNames').filter((value): value is string => typeof value === 'string') // Collect extra string fields const knownKeys = new Set(['folderName', 'files', 'fileTypes', 'textureNames']) diff --git a/lib/upload-api.ts b/lib/upload-api.ts index 06ee006..049503a 100644 --- a/lib/upload-api.ts +++ b/lib/upload-api.ts @@ -16,6 +16,20 @@ export interface StageResult { filesCount: number } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function getApiError(data: unknown, fallback: string) { + return isRecord(data) && typeof data.error === 'string' ? data.error : fallback +} + +function isFileDiff(value: unknown): value is FileDiff { + return isRecord(value) + && typeof value.name === 'string' + && (value.status === 'new' || value.status === 'changed' || value.status === 'deleted') +} + // --------------------------------------------------------------------------- // Shared FormData builder // --------------------------------------------------------------------------- @@ -69,18 +83,20 @@ export async function checkFolderDiffs( signal, }) - const data = await res.json() + const data: unknown = await res.json() // Surface auth/server errors to the caller if (!res.ok) { - throw new Error(data.error || `Erreur serveur (${res.status})`) + throw new Error(getApiError(data, `Erreur serveur (${res.status})`)) } - if (!data.success || !data.exists) { + if (!isRecord(data) || data.success !== true || data.exists !== true) { return { exists: false, diffs: [] } } - return { exists: true, diffs: (data.diffs || []) as FileDiff[] } + const diffs = Array.isArray(data.diffs) ? data.diffs.filter(isFileDiff) : [] + + return { exists: true, diffs } } export async function stageUpload( @@ -96,10 +112,14 @@ export async function stageUpload( signal, }) - const data = await res.json() + const data: unknown = await res.json() - if (!res.ok || !data.success) { - throw new Error(data.error || `Erreur serveur (${res.status})`) + if (!res.ok || !isRecord(data) || data.success !== true) { + throw new Error(getApiError(data, `Erreur serveur (${res.status})`)) + } + + if (typeof data.stagingId !== 'string' || typeof data.folderName !== 'string' || typeof data.filesCount !== 'number') { + throw new Error('Reponse serveur invalide') } return { @@ -130,8 +150,10 @@ export async function uploadDrive( body: JSON.stringify({ stagingId, action }), signal, }) - const data = await res.json() - if (!data.success) return { success: false, error: data.error } + const data: unknown = await res.json() + if (!res.ok || !isRecord(data) || data.success !== true) { + return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) } + } return { success: true } } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') { @@ -166,14 +188,14 @@ export async function uploadGit( }) onProgress(80) - const data = await res.json() + const data: unknown = await res.json() - if (!data.success) { - return { success: false, error: data.error } + if (!res.ok || !isRecord(data) || data.success !== true) { + return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) } } onProgress(100) - return { success: true, filename: data.folderName } + return { success: true, filename: typeof data.folderName === 'string' ? data.folderName : undefined } } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') { return { success: false, error: 'Upload annule' } diff --git a/lib/upload-request.ts b/lib/upload-request.ts new file mode 100644 index 0000000..c194b6f --- /dev/null +++ b/lib/upload-request.ts @@ -0,0 +1,28 @@ +export type DriveAction = 'new' | 'replace' + +interface StagingRequestBody { + stagingId: string +} + +interface DriveRequestBody extends StagingRequestBody { + action: DriveAction +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +export function parseStagingRequestBody(value: unknown): StagingRequestBody { + if (!isRecord(value) || typeof value.stagingId !== 'string' || value.stagingId.trim() === '') { + throw new Error('stagingId manquant') + } + + return { stagingId: value.stagingId } +} + +export function parseDriveRequestBody(value: unknown): DriveRequestBody { + const { stagingId } = parseStagingRequestBody(value) + const action = isRecord(value) && value.action === 'replace' ? 'replace' : 'new' + + return { stagingId, action } +}