Files
upload-gltf/hooks/useUploadOrchestrator.ts
T
2026-04-27 23:17:56 +02:00

384 lines
12 KiB
TypeScript

'use client'
// ---------------------------------------------------------------------------
// Upload orchestration hook — manages the Drive→Git upload pipeline
// ---------------------------------------------------------------------------
import { useState, useRef, useCallback } from 'react'
import type { FolderEntry } from '@/lib/client-types'
import type { FileDiff } from '@/lib/types'
import { checkFolderDiffs, stageUpload, uploadDrive, uploadGit } from '@/lib/upload-api'
import type { CheckResult } from '@/lib/upload-api'
function formatElapsed(startedAt: number) {
return `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
}
function logUpload(level: 'INFO' | 'ERROR', step: string, action: string, startedAt: number, details?: Record<string, unknown>) {
const log = level === 'ERROR' ? console.error : console.info
log(`[${level}] ${step} -> ${action} | Timer: ${formatElapsed(startedAt)}`, details || '')
}
function startTimedLog(step: string, action: string, details?: Record<string, unknown>) {
const startedAt = performance.now()
logUpload('INFO', step, `${action} started`, startedAt, details)
const interval = window.setInterval(() => {
logUpload('INFO', step, `${action} running`, startedAt, details)
}, 10_000)
return (status: 'done' | 'failed' | 'cancelled' = 'done', extra?: Record<string, unknown>) => {
window.clearInterval(interval)
logUpload(status === 'failed' ? 'ERROR' : 'INFO', step, `${action} ${status}`, startedAt, { ...details, ...extra })
}
}
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 [isChecking, setIsChecking] = useState(false)
const [isResolvingDriveError, setIsResolvingDriveError] = useState(false)
const [globalError, setGlobalError] = useState<string | 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: [] })
const uploadActionRef = useRef(false)
const stagingIdRef = useRef<string | null>(null)
// Refs for values used inside callbacks to avoid stale closures
const secretRef = useRef(secret)
secretRef.current = secret
const entriesRef = useRef(entries)
entriesRef.current = entries
// ---- Internal: push a single folder to Git ----
const pushGit = useCallback(async (index: number, signal?: AbortSignal) => {
const stagingId = stagingIdRef.current
if (!stagingId) {
updateEntry(index, { status: 'error', error: 'Preparation serveur introuvable' })
return
}
const folderName = entriesRef.current[index]?.folderName
const endGitLog = startTimedLog('Git', 'Upload', { folderName, stagingId })
let gitResult: Awaited<ReturnType<typeof uploadGit>>
try {
gitResult = await uploadGit(
stagingId,
secretRef.current,
(pct) => updateEntry(index, { progress: 50 + Math.round(pct / 2) }),
signal,
)
endGitLog(gitResult.success ? 'done' : 'failed', { error: gitResult.error })
} catch (err) {
endGitLog(signal?.aborted ? 'cancelled' : 'failed', {
error: err instanceof Error ? err.message : 'Erreur inconnue',
})
throw err
}
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 () => {
if (uploadActionRef.current) return
uploadActionRef.current = true
setOverwriteConfirm(null)
setIsChecking(false)
setIsUploading(true)
setGlobalError(null)
const controller = new AbortController()
abortRef.current = controller
try {
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'
const stagingId = stagingIdRef.current
if (!stagingId) {
updateEntry(i, { status: 'error', error: 'Preparation serveur introuvable' })
return
}
// ---- Step 1: Drive upload ----
updateEntry(i, {
status: 'uploading',
progress: 1,
error: undefined,
driveStatus: 'uploading',
driveError: undefined,
})
const endDriveLog = startTimedLog('Drive', 'Upload', {
folderName: folderEntry.folderName,
stagingId,
action: driveAction,
})
let driveResult: Awaited<ReturnType<typeof uploadDrive>>
try {
driveResult = await uploadDrive(
stagingId,
secretRef.current,
driveAction as 'new' | 'replace',
controller.signal,
)
endDriveLog(driveResult.success ? 'done' : 'failed', { error: driveResult.error })
} catch (err) {
endDriveLog(controller.signal.aborted ? 'cancelled' : 'failed', {
error: err instanceof Error ? err.message : 'Erreur inconnue',
})
throw err
}
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)
}
} finally {
abortRef.current = null
setIsUploading(false)
uploadActionRef.current = false
}
}, [updateEntry, pushGit])
// ---- Handlers ----
const handleUpload = useCallback(async () => {
if (uploadActionRef.current || isChecking || isUploading) return
if (!secretRef.current.trim()) {
setSecretError("La cle d'acces est requise")
return
}
if (entriesRef.current.length === 0) return
uploadActionRef.current = true
setIsChecking(true)
setSecretError(null)
setGlobalError(null)
const folder = entriesRef.current[0]
const controller = new AbortController()
abortRef.current = controller
try {
const endStageLog = startTimedLog('Verification', 'Staging', {
folderName: folder.folderName,
files: 1 + folder.textures.length,
modelSize: folder.modelFile.size,
})
let staged: Awaited<ReturnType<typeof stageUpload>>
try {
staged = await stageUpload(folder, secretRef.current, controller.signal)
endStageLog('done', { stagingId: staged.stagingId, filesCount: staged.filesCount })
} catch (err) {
endStageLog(controller.signal.aborted ? 'cancelled' : 'failed', {
error: err instanceof Error ? err.message : 'Erreur inconnue',
})
throw err
}
stagingIdRef.current = staged.stagingId
const endCheckLog = startTimedLog('Verification', 'GitHub diff', {
folderName: folder.folderName,
stagingId: staged.stagingId,
})
let check: CheckResult
try {
check = await checkFolderDiffs(
staged.stagingId,
secretRef.current,
controller.signal,
)
endCheckLog('done', { exists: check.exists, diffs: check.diffs.length })
} catch (err) {
endCheckLog(controller.signal.aborted ? 'cancelled' : 'failed', {
error: err instanceof Error ? err.message : 'Erreur inconnue',
})
throw err
}
checkResultRef.current = check
if (check.exists) {
if (check.diffs.length === 0) {
setNoChangesFolder(folder.folderName)
uploadActionRef.current = false
setIsChecking(false)
abortRef.current = null
return
}
setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs })
uploadActionRef.current = false
setIsChecking(false)
abortRef.current = null
return
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Erreur inconnue'
setGlobalError(message)
uploadActionRef.current = false
setIsChecking(false)
abortRef.current = null
return
}
uploadActionRef.current = false
abortRef.current = null
await proceedUpload()
}, [setSecretError, proceedUpload, isChecking, isUploading])
const handleDriveContinue = useCallback(async () => {
if (!driveError || uploadActionRef.current) return
uploadActionRef.current = true
setIsResolvingDriveError(true)
const idx = driveError.folderIndex
setDriveError(null)
try {
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)
}
} finally {
abortRef.current = null
setIsUploading(false)
setIsResolvingDriveError(false)
uploadActionRef.current = 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
uploadActionRef.current = false
setIsChecking(false)
setIsResolvingDriveError(false)
setIsUploading(false)
}, [driveError, updateEntry])
const handleCancel = useCallback(() => {
if (isChecking) {
abortRef.current?.abort()
abortRef.current = null
uploadActionRef.current = false
setIsChecking(false)
return
}
abortRef.current?.abort()
abortRef.current = null
uploadActionRef.current = false
setIsResolvingDriveError(false)
setIsUploading(false)
stagingIdRef.current = null
}, [isChecking])
const handleReset = useCallback(() => {
resetEntries()
setGlobalError(null)
setIsChecking(false)
setIsUploading(false)
setIsResolvingDriveError(false)
setDriveError(null)
checkResultRef.current = { exists: false, diffs: [] }
uploadActionRef.current = false
stagingIdRef.current = null
}, [resetEntries])
return {
isUploading,
isChecking,
isResolvingDriveError,
globalError,
setGlobalError,
overwriteConfirm,
setOverwriteConfirm,
noChangesFolder,
setNoChangesFolder,
driveError,
handleUpload,
handleOverwriteCancel: () => {
setOverwriteConfirm(null)
uploadActionRef.current = false
setIsChecking(false)
},
handleDriveContinue,
handleDriveCancel,
handleCancel,
handleReset,
proceedUpload,
}
}