import { getErrorMessage, isRecord } from './guards' import type { FolderEntry } from './client-types' import type { CheckUploadResult, DriveAction, DriveUploadResult, FileDiff, GitModelMode, GitUploadResult, StagingUploadResult, } from './types' interface CompressionWarningPayload { compressionError?: unknown } interface SuccessfulUploadData extends CompressionWarningPayload { success: true exists?: unknown diffs?: unknown stagingId?: unknown folderName?: unknown filesCount?: unknown } type UploadJsonBody = | { stagingId: string } | { stagingId: string; action: DriveAction } function getApiError(data: unknown, fallback: string) { return isRecord(data) && typeof data.error === 'string' ? data.error : fallback } function getClientRequestError(err: unknown, label: string) { return `${label}: ${getErrorMessage(err)}` } function getCompressionWarning(data: CompressionWarningPayload) { if (typeof data.compressionError !== 'string') return undefined return `Compression GLB impossible. Le modele a ete prepare en GLTF separe. Detail : ${data.compressionError}` } function getUploadJsonHeaders(secret: string) { return { 'Content-Type': 'application/json', 'x-upload-secret': secret.trim(), } } async function postUploadJson( endpoint: string, secret: string, body: UploadJsonBody, signal?: AbortSignal, ) { const res = await fetch(endpoint, { method: 'POST', headers: getUploadJsonHeaders(secret), body: JSON.stringify(body), signal, }) const data: unknown = await res.json() return { res, data } } function isSuccessfulUploadData(data: unknown): data is SuccessfulUploadData { return isRecord(data) && data.success === true } function isAbortError(err: unknown) { return err instanceof DOMException && err.name === 'AbortError' } function getNetworkUploadError(err: unknown, label: string) { return isAbortError(err) ? 'Upload annule' : getClientRequestError(err, label) } function isFileDiff(value: unknown): value is FileDiff { return isRecord(value) && typeof value.name === 'string' && (value.status === 'new' || value.status === 'changed' || value.status === 'deleted') } function parseFileDiffs(value: unknown): FileDiff[] { if (!Array.isArray(value)) { throw new Error('Reponse serveur invalide') } const diffs: FileDiff[] = [] for (const diff of value) { if (!isFileDiff(diff)) { throw new Error('Reponse serveur invalide') } diffs.push(diff) } return diffs } function buildUploadFormData(folder: FolderEntry, gitModelMode: GitModelMode): FormData { const formData = new FormData() formData.append('folderName', folder.folderName) formData.append('gitModelMode', gitModelMode) formData.append('files', folder.modelFile) formData.append('fileTypes', 'model') formData.append('textureNames', '') for (const tex of folder.textures) { formData.append('files', tex.file) formData.append('fileTypes', 'texture') formData.append('textureNames', tex.name) } return formData } export async function checkFolderDiffs( stagingId: string, secret: string, signal?: AbortSignal, ): Promise { const { res, data } = await postUploadJson('/api/upload/check', secret, { stagingId }, signal) if (!res.ok) { throw new Error(getApiError(data, `Erreur serveur (${res.status})`)) } if (!isSuccessfulUploadData(data)) { throw new Error('Reponse serveur invalide') } const warning = getCompressionWarning(data) if (data.exists !== true) { return { exists: false, diffs: [], warning, } } return { exists: true, diffs: parseFileDiffs(data.diffs), warning, } } export async function stageUpload( folder: FolderEntry, gitModelMode: GitModelMode, secret: string, signal?: AbortSignal, ): Promise { const formData = buildUploadFormData(folder, gitModelMode) const res = await fetch('/api/upload/stage', { method: 'POST', headers: { 'x-upload-secret': secret.trim() }, body: formData, signal, }) const data: unknown = await res.json() if (!res.ok || !isSuccessfulUploadData(data)) { 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 { stagingId: data.stagingId, folderName: data.folderName, filesCount: data.filesCount, } } export async function uploadDrive( stagingId: string, secret: string, action: DriveAction, signal?: AbortSignal, ): Promise { try { const { res, data } = await postUploadJson('/api/upload/drive', secret, { stagingId, action }, signal) if (!res.ok || !isSuccessfulUploadData(data)) { return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) } } return { success: true } } catch (err) { return { success: false, error: getNetworkUploadError(err, 'Erreur Drive') } } } export async function uploadGit( stagingId: string, secret: string, onProgress: (pct: number) => void, signal?: AbortSignal, ): Promise { onProgress(10) try { const { res, data } = await postUploadJson('/api/upload/git', secret, { stagingId }, signal) onProgress(80) if (!res.ok || !isSuccessfulUploadData(data)) { return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) } } onProgress(100) return { success: true, filename: typeof data.folderName === 'string' ? data.folderName : undefined, warning: getCompressionWarning(data), } } catch (err) { return { success: false, error: getNetworkUploadError(err, 'Erreur GitHub') } } }