import { Track } from '../types/track'; import * as trackService from '../services/uploadService'; /** * Taille par défaut d'un chunk (5MB) */ export const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB /** * Seuil pour utiliser le chunked upload (100MB) */ const CHUNKED_UPLOAD_THRESHOLD = 10 * 1024 * 1024; // 10MB /** * Divise un fichier en chunks * @param file Fichier à diviser * @param chunkSize Taille de chaque chunk * @returns Tableau de Blobs représentant les chunks */ export function splitFileIntoChunks( file: File, chunkSize: number = CHUNK_SIZE, ): Blob[] { const chunks: Blob[] = []; let start = 0; while (start < file.size) { const end = Math.min(start + chunkSize, file.size); chunks.push(file.slice(start, end)); start = end; } return chunks; } /** * Calcule le nombre total de chunks nécessaires * @param totalSize Taille totale du fichier * @param chunkSize Taille de chaque chunk * @returns Nombre total de chunks */ export function calculateTotalChunks( totalSize: number, chunkSize: number = CHUNK_SIZE, ): number { return Math.ceil(totalSize / chunkSize); } /** * État d'un upload par chunks */ export interface ChunkedUploadState { uploadId: string | null; totalChunks: number; uploadedChunks: number; progress: number; isPaused: boolean; isComplete: boolean; error: string | null; track: Track | null; } /** * Gestionnaire d'upload par chunks * Gère l'upload d'un fichier volumineux en le divisant en chunks */ export class ChunkedUploadManager { private file: File; private chunks: Blob[]; private state: ChunkedUploadState; private onProgress?: (progress: number) => void; private isCancelled = false; private currentChunkIndex = 0; private readonly MAX_RETRIES = 3; constructor(file: File, onProgress?: (progress: number) => void) { this.file = file; this.chunks = splitFileIntoChunks(file, CHUNK_SIZE); this.onProgress = onProgress; this.state = { uploadId: null, totalChunks: this.chunks.length, uploadedChunks: 0, progress: 0, isPaused: false, isComplete: false, error: null, track: null, }; } /** * Démarre l'upload par chunks */ async start(): Promise { if (this.state.isComplete) { if (this.state.track) { return this.state.track; } throw new Error('Upload already completed but no track available'); } if (this.isCancelled) { throw new Error('Upload was cancelled'); } try { // 1. Initier l'upload if (!this.state.uploadId) { const uploadId = await trackService.initiateChunkedUpload( this.state.totalChunks, this.file.size, this.file.name, ); this.state.uploadId = uploadId; } // 2. Uploader chaque chunk séquentiellement for (let i = this.currentChunkIndex; i < this.chunks.length; i++) { if (this.isCancelled || this.state.isPaused) { break; } this.currentChunkIndex = i; const chunk = this.chunks[i]; const chunkNumber = i + 1; // 1-based // Retry logic pour chaque chunk let success = false; let lastError: Error | null = null; for (let retry = 0; retry <= this.MAX_RETRIES; retry++) { try { const response = await trackService.uploadChunk( this.state.uploadId!, chunkNumber, this.state.totalChunks, this.file.size, this.file.name, chunk, (chunkProgress) => { // Progression globale = (chunks uploadés + progression du chunk actuel) / total const globalProgress = ((i * 100 + chunkProgress) / this.state.totalChunks) * 0.9; // 90% pour l'upload, 10% pour l'assemblage this.updateProgress(globalProgress); }, ); // Le backend retourne la progression dans la réponse this.state.uploadedChunks = response.received_chunks; this.updateProgress(response.progress * 0.9); // 90% pour l'upload success = true; break; } catch (error) { lastError = error as Error; if (retry < this.MAX_RETRIES) { // Attendre avant de réessayer (backoff exponentiel) await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry) * 1000), ); } } } if (!success) { throw lastError || new Error('Failed to upload chunk after retries'); } } // 3. Vérifier que tous les chunks ont été uploadés if (this.isCancelled) { throw new Error('Upload was cancelled'); } if (this.state.uploadedChunks < this.state.totalChunks) { throw new Error( `Not all chunks uploaded: ${this.state.uploadedChunks}/${this.state.totalChunks}`, ); } // 4. Compléter l'upload this.updateProgress(95); const track = await trackService.completeChunkedUpload( this.state.uploadId!, ); this.state.track = track; this.state.isComplete = true; this.updateProgress(100); return track; } catch (error) { this.state.error = error instanceof Error ? error.message : 'Unknown error occurred'; throw error; } } /** * Met en pause l'upload */ pause(): void { this.state.isPaused = true; } /** * Reprend l'upload */ async resume(): Promise { if (this.state.isComplete) { if (this.state.track) { return this.state.track; } throw new Error('Upload already completed but no track available'); } this.state.isPaused = false; return this.start(); } /** * Annule l'upload */ cancel(): void { this.isCancelled = true; this.state.isPaused = true; this.state.error = 'Upload cancelled'; } /** * Retourne l'état actuel de l'upload */ getState(): ChunkedUploadState { return { ...this.state }; } /** * Met à jour la progression et appelle le callback */ private updateProgress(progress: number): void { this.state.progress = Math.min(100, Math.max(0, progress)); if (this.onProgress) { this.onProgress(this.state.progress); } } } /** * Détermine si un fichier doit utiliser le chunked upload * @param file Fichier à vérifier * @returns true si le fichier est > 100MB */ export function shouldUseChunkedUpload(file: File): boolean { return file.size > CHUNKED_UPLOAD_THRESHOLD; }