refactor: full codebase audit — extract modules, fix type safety, clean dead code

- Extract API helpers from UploadZone into lib/upload-api.ts (FormData builder, checkFolderDiffs, uploadDrive, uploadGit)
- Extract upload orchestration into hooks/useUploadOrchestrator.ts (UploadZone: 489 → 162 lines)
- Extract file diff classification into lib/diff-files.ts (from git route)
- Extract shared SVG icons into components/ui/icons.tsx (7 icons, 0 duplication)
- Extract shared modal wrapper into components/ui/Modal.tsx + ModalActions
- Extract DriveStatusLine sub-component from FolderCard
- Fix checkFolderDiffs silently swallowing auth/network errors (now throws)
- Fix type safety: remove as never casts, add isHttpError type guard, use discriminated union for validateFolder
- Fix nextcloud: cache getConfig, add max bound to findNextVersion, optimize mkdirRecursive (skip PROPFIND)
- Fix drive route: remove req.clone(), extend parseMultiUpload to return extra fields
- Fix commit message: model shown as unchanged with ↔️ on updates (not falsely marked as modified)
- Clean dead code: unused folderExists import, FileStatus/DriveStatus exports, ParsedFile.textureName, getConfig basePath
- Add security headers in next.config.ts (HSTS, X-Content-Type-Options, X-Frame-Options, etc.)
- Update README with new project structure
This commit is contained in:
Tom Boullay
2026-04-14 17:19:10 +02:00
parent 110d64ec33
commit 78f4aa83e0
26 changed files with 957 additions and 721 deletions
+247
View File
@@ -0,0 +1,247 @@
'use client'
// ---------------------------------------------------------------------------
// Upload orchestration hook — manages the Drive→Git upload pipeline
// ---------------------------------------------------------------------------
import { useState, useRef, useCallback } from 'react'
import type { Destination } from '@/lib/constants'
import type { FolderEntry } from '@/lib/client-types'
import type { FileDiff } from '@/lib/types'
import { checkFolderDiffs, uploadDrive, uploadGit } from '@/lib/upload-api'
import type { CheckResult } from '@/lib/upload-api'
interface UseUploadOrchestratorParams {
secret: string
setSecretError: (err: string | null) => void
entries: FolderEntry[]
updateEntry: (index: number, patch: Partial<FolderEntry>) => void
resetEntries: () => void
}
export function useUploadOrchestrator({
secret,
setSecretError,
entries,
updateEntry,
resetEntries,
}: UseUploadOrchestratorParams) {
const [isUploading, setIsUploading] = useState(false)
const [globalError, setGlobalError] = useState<string | null>(null)
const [destination, setDestination] = useState<Destination | null>(null)
const [overwriteConfirm, setOverwriteConfirm] = useState<{
folderName: string
diffs: FileDiff[]
} | null>(null)
const [noChangesFolder, setNoChangesFolder] = useState<string | null>(null)
const [driveError, setDriveError] = useState<{
error: string
folderIndex: number
} | null>(null)
const abortRef = useRef<AbortController | null>(null)
const checkResultRef = useRef<CheckResult>({ exists: false, diffs: [] })
// Refs for values used inside callbacks to avoid stale closures
const secretRef = useRef(secret)
secretRef.current = secret
const destinationRef = useRef(destination)
destinationRef.current = destination
const entriesRef = useRef(entries)
entriesRef.current = entries
// ---- Internal: push a single folder to Git ----
const pushGit = useCallback(async (index: number, signal?: AbortSignal) => {
const folderEntry = entriesRef.current[index]
const dest = destinationRef.current
const gitResult = await uploadGit(
folderEntry,
secretRef.current,
dest!,
(pct) => updateEntry(index, { progress: 50 + Math.round(pct / 2) }),
signal,
)
updateEntry(index, {
status: gitResult.success ? 'success' : 'error',
progress: gitResult.success ? 100 : 0,
error: gitResult.success ? undefined : gitResult.error,
filename: gitResult.filename,
})
}, [updateEntry])
// ---- Main upload flow: Drive first, then Git ----
const proceedUpload = useCallback(async () => {
setOverwriteConfirm(null)
setIsUploading(true)
setGlobalError(null)
const controller = new AbortController()
abortRef.current = controller
const currentEntries = entriesRef.current
for (let i = 0; i < currentEntries.length; i++) {
if (currentEntries[i].status === 'success') continue
if (controller.signal.aborted) break
const folderEntry = currentEntries[i]
const driveAction = checkResultRef.current.exists ? 'replace' : 'new'
// ---- Step 1: Drive upload ----
updateEntry(i, {
status: 'uploading',
progress: 1,
error: undefined,
driveStatus: 'uploading',
driveError: undefined,
})
const driveResult = await uploadDrive(
folderEntry,
secretRef.current,
destinationRef.current!,
driveAction as 'new' | 'replace',
controller.signal,
)
if (!driveResult.success) {
updateEntry(i, { driveStatus: 'error', driveError: driveResult.error })
setDriveError({ error: driveResult.error || 'Erreur inconnue', folderIndex: i })
return
}
updateEntry(i, { driveStatus: 'success', progress: 50 })
// ---- Step 2: Git upload ----
await pushGit(i, controller.signal)
}
abortRef.current = null
setIsUploading(false)
}, [updateEntry, pushGit])
// ---- Handlers ----
const handleUpload = useCallback(async () => {
if (!secretRef.current.trim()) {
setSecretError("La cle d'acces est requise")
return
}
if (!destinationRef.current) {
setGlobalError('Veuillez choisir une destination')
return
}
if (entriesRef.current.length === 0) return
setSecretError(null)
setGlobalError(null)
const folder = entriesRef.current[0]
try {
const check = await checkFolderDiffs(
folder,
destinationRef.current,
secretRef.current,
abortRef.current?.signal,
)
checkResultRef.current = check
if (check.exists) {
if (check.diffs.length === 0) {
setNoChangesFolder(folder.folderName)
return
}
setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs })
return
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Erreur inconnue'
setGlobalError(message)
return
}
await proceedUpload()
}, [setSecretError, proceedUpload])
const handleDriveContinue = useCallback(async () => {
if (!driveError) return
const idx = driveError.folderIndex
setDriveError(null)
updateEntry(idx, { driveStatus: 'skipped' })
const signal = abortRef.current?.signal
await pushGit(idx, signal)
const currentEntries = entriesRef.current
for (let i = idx + 1; i < currentEntries.length; i++) {
if (currentEntries[i].status === 'success') continue
if (abortRef.current?.signal.aborted) break
updateEntry(i, {
status: 'uploading',
progress: 0,
error: undefined,
driveStatus: 'skipped',
})
await pushGit(i, abortRef.current?.signal)
}
abortRef.current = null
setIsUploading(false)
}, [driveError, updateEntry, pushGit])
const handleDriveCancel = useCallback(() => {
if (!driveError) return
const idx = driveError.folderIndex
setDriveError(null)
updateEntry(idx, {
status: 'error',
progress: 0,
error: 'Upload annule (Drive echoue)',
driveStatus: 'error',
})
abortRef.current?.abort()
abortRef.current = null
setIsUploading(false)
}, [driveError, updateEntry])
const handleCancel = useCallback(() => {
abortRef.current?.abort()
abortRef.current = null
setIsUploading(false)
}, [])
const handleReset = useCallback(() => {
resetEntries()
setGlobalError(null)
setIsUploading(false)
setDriveError(null)
checkResultRef.current = { exists: false, diffs: [] }
}, [resetEntries])
return {
isUploading,
globalError,
setGlobalError,
destination,
setDestination,
overwriteConfirm,
setOverwriteConfirm,
noChangesFolder,
setNoChangesFolder,
driveError,
handleUpload,
handleDriveContinue,
handleDriveCancel,
handleCancel,
handleReset,
proceedUpload,
}
}