import { THUMB_TONES } from '../lib/gradients'; import { formatBytes, formatRelativeTime } from '../lib/format'; import type { FileItem, FileKind, StatItem } from '../types'; import { apiFetch } from './client'; interface FileView { id: string; filename: string; content_type: string; category: string; size_bytes: number; created_at: string; } interface StorageView { used_bytes: number; available_bytes: number; } function kindOf(view: FileView): FileKind { if (view.category === 'backup') return 'backup'; if (view.content_type.startsWith('image/')) return 'image'; if (view.content_type.startsWith('video/')) return 'video'; if ( view.content_type.includes('zip') || view.content_type.includes('gzip') || view.content_type.includes('tar') || view.content_type.includes('compressed') ) { return 'archive'; } return 'document'; } function extLabelOf(view: FileView, kind: FileKind): string { const ext = view.filename.includes('.') ? view.filename.split('.').pop()!.toUpperCase() : ''; const byKind: Record = { image: 'Image', video: 'Vidéo', archive: 'Archive', backup: 'Sauvegarde', document: 'Document', folder: 'Dossier', }; return ext ? `${byKind[kind]} ${ext}` : byKind[kind]; } function toFileItem(view: FileView, index: number): FileItem { const kind = kindOf(view); return { id: view.id, name: view.filename, kind, extLabel: extLabelOf(view, kind), size: formatBytes(view.size_bytes), modified: formatRelativeTime(view.created_at), thumbGradient: THUMB_TONES[index % THUMB_TONES.length]!, }; } export async function getFiles(category?: string): Promise { const qs = category ? `?category=${encodeURIComponent(category)}` : ''; const views = await apiFetch(`/api/files${qs}`); return views.map(toFileItem); } export function downloadUrl(fileId: string): string { return `/api/files/${fileId}`; } export async function deleteFile(fileId: string): Promise { await apiFetch(`/api/files/${fileId}`, { method: 'DELETE' }); } /** * Upload réel via XMLHttpRequest (pas `fetch`) : c'est la seule API du * navigateur qui expose un vrai événement de progression sur le corps * envoyé (`upload.onprogress`) - `fetch` n'a pas d'équivalent côté * requête sortante. */ export function uploadFile( file: File, category: 'file' | 'photo' | 'backup', onProgress: (percent: number) => void, ): Promise { return new Promise((resolve, reject) => { const form = new FormData(); form.append('category', category); form.append('file', file); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/files'); xhr.withCredentials = true; xhr.upload.onprogress = (e) => { if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100)); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { const view = JSON.parse(xhr.responseText) as FileView; resolve(toFileItem(view, 0)); } else { let message = xhr.statusText; try { message = (JSON.parse(xhr.responseText) as { error?: string }).error ?? message; } catch { // pas de corps JSON exploitable } reject(new Error(message)); } }; xhr.onerror = () => reject(new Error('Échec réseau pendant le transfert')); xhr.send(form); }); } export async function getDashboardStats(): Promise { const [files, storage] = await Promise.all([ apiFetch('/api/files'), apiFetch('/api/storage'), ]); return [ { id: 'stat_storage', label: 'Espace utilisé', value: formatBytes(storage.used_bytes), note: `${formatBytes(storage.available_bytes)} disponibles sur le serveur`, icon: 'hard-drive', tone: 'brand', }, { id: 'stat_docs', label: 'Fichiers', value: String(files.length), note: files.length > 0 ? `dernier dépôt ${formatRelativeTime(files[0]!.created_at)}` : 'aucun dépôt', icon: 'image', tone: 'success', }, ]; }