78f4aa83e0
- 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
248 lines
6.8 KiB
TypeScript
248 lines
6.8 KiB
TypeScript
'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,
|
|
}
|
|
}
|