// --------------------------------------------------------------------------- // Client-side API helpers for upload operations // --------------------------------------------------------------------------- import type { FolderEntry } from './client-types' import type { FileDiff } from './types' export interface CheckResult { exists: boolean diffs: FileDiff[] } export interface StageResult { stagingId: string folderName: string 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 // --------------------------------------------------------------------------- function buildUploadFormData( folder: FolderEntry, extra?: Record, ): FormData { const formData = new FormData() formData.append('folderName', folder.folderName) if (extra) { for (const [key, value] of Object.entries(extra)) { formData.append(key, value) } } 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 } // --------------------------------------------------------------------------- // Check folder diffs against remote (GitHub) // --------------------------------------------------------------------------- /** * Check whether a folder already exists on the remote repo and compute diffs. * Throws on auth/network errors so callers can surface them to the user. */ export async function checkFolderDiffs( stagingId: string, secret: string, signal?: AbortSignal, ): Promise { const res = await fetch('/api/upload/check', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-upload-secret': secret.trim(), }, body: JSON.stringify({ stagingId }), signal, }) const data: unknown = await res.json() // Surface auth/server errors to the caller if (!res.ok) { throw new Error(getApiError(data, `Erreur serveur (${res.status})`)) } if (!isRecord(data) || data.success !== true || data.exists !== true) { return { exists: false, diffs: [] } } const diffs = Array.isArray(data.diffs) ? data.diffs.filter(isFileDiff) : [] return { exists: true, diffs } } export async function stageUpload( folder: FolderEntry, secret: string, signal?: AbortSignal, ): Promise { const formData = buildUploadFormData(folder) 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 || !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 { stagingId: data.stagingId, folderName: data.folderName, filesCount: data.filesCount, } } // --------------------------------------------------------------------------- // Upload original files to Nextcloud Drive // --------------------------------------------------------------------------- /** Upload original files to Nextcloud Drive. */ export async function uploadDrive( stagingId: string, secret: string, action: 'new' | 'replace', signal?: AbortSignal, ): Promise<{ success: boolean; error?: string }> { try { const res = await fetch('/api/upload/drive', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-upload-secret': secret.trim(), }, body: JSON.stringify({ stagingId, action }), signal, }) 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') { return { success: false, error: 'Upload annule' } } return { success: false, error: 'Erreur reseau (Drive)' } } } // --------------------------------------------------------------------------- // Upload files to GitHub // --------------------------------------------------------------------------- /** Upload files to GitHub. */ export async function uploadGit( stagingId: string, secret: string, onProgress: (pct: number) => void, signal?: AbortSignal, ): Promise<{ success: boolean; filename?: string; error?: string }> { onProgress(10) try { const res = await fetch('/api/upload/git', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-upload-secret': secret.trim(), }, body: JSON.stringify({ stagingId }), signal, }) onProgress(80) const data: unknown = await res.json() if (!res.ok || !isRecord(data) || data.success !== true) { return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) } } onProgress(100) 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' } } return { success: false, error: 'Erreur reseau' } } }