veza/apps/web/src/features/tracks/services/trackDownloadService.ts
senke 463109c4e0 fix(INT-000002): Multiple Auth Storage Mechanisms
- Unified token storage to use TokenStorage service
- Removed deprecated token-manager.ts
- Removed fallback storage logic in API client
- Updated tests and feature components to use TokenStorage

Resolves: INT-000002
Severity: P0
2025-12-22 09:53:47 -05:00

243 lines
6.7 KiB
TypeScript

import { apiClient } from '@/services/api/client';
import { TokenStorage } from '@/services/tokenStorage';
/**
* Track Download Service
* T0314: Service frontend pour télécharger des tracks
*/
/**
* Classe d'erreur personnalisée pour le téléchargement de tracks
*/
export class TrackDownloadError extends Error {
constructor(
message: string,
public code:
| 'VALIDATION'
| 'NETWORK'
| 'SERVER'
| 'FORBIDDEN'
| 'NOT_FOUND'
| 'UNKNOWN',
public retryable: boolean = false,
public originalError?: unknown,
) {
super(message);
this.name = 'TrackDownloadError';
}
}
/**
* Options pour le téléchargement d'un track
*/
export interface DownloadTrackOptions {
shareToken?: string;
filename?: string;
onProgress?: (progress: number) => void;
}
/**
* Télécharge un track
* @param trackId ID du track à télécharger
* @param options Options de téléchargement (shareToken, filename, onProgress)
* @throws TrackDownloadError si le téléchargement échoue
*/
export async function downloadTrack(
trackId: number,
options: DownloadTrackOptions = {},
): Promise<void> {
const { shareToken, filename, onProgress } = options;
try {
// Construire l'URL avec le share token si fourni
let url = `/tracks/${trackId}/download`;
if (shareToken) {
url += `?share_token=${encodeURIComponent(shareToken)}`;
}
// Obtenir le token d'authentification
const token = TokenStorage.getAccessToken();
const baseURL =
apiClient.defaults.baseURL || 'http://localhost:8080/api/v1';
const fullUrl = `${baseURL}${url}`;
// Créer la requête fetch
const headers: HeadersInit = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Utiliser fetch pour télécharger le fichier avec support du progress
const response = await fetch(fullUrl, {
method: 'GET',
headers,
});
if (!response.ok) {
// Essayer de parser l'erreur JSON
let errorMessage = 'Échec du téléchargement';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch {
// Si ce n'est pas du JSON, utiliser le message par défaut
}
if (response.status === 400) {
throw new TrackDownloadError(errorMessage, 'VALIDATION', false);
}
if (response.status === 401) {
throw new TrackDownloadError(
'Non autorisé: Veuillez vous connecter pour télécharger ce track',
'VALIDATION',
false,
);
}
if (response.status === 403) {
throw new TrackDownloadError(
errorMessage ||
"Accès refusé: Vous n'avez pas la permission de télécharger ce track",
'FORBIDDEN',
false,
);
}
if (response.status === 404) {
throw new TrackDownloadError('Track introuvable', 'NOT_FOUND', false);
}
if (response.status === 500) {
throw new TrackDownloadError(
'Erreur serveur: Impossible de télécharger le track. Veuillez réessayer plus tard.',
'SERVER',
true,
);
}
throw new TrackDownloadError(errorMessage, 'UNKNOWN', false);
}
// Gérer le téléchargement avec progress si supporté
if (onProgress && response.body) {
await downloadWithProgress(response, filename, onProgress);
} else {
// Téléchargement simple sans progress
const blob = await response.blob();
await triggerDownload(
blob,
filename || getFilenameFromResponse(response),
);
}
} catch (error) {
if (error instanceof TrackDownloadError) {
throw error;
}
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new TrackDownloadError(
'Erreur réseau: Impossible de se connecter au serveur. Veuillez vérifier votre connexion.',
'NETWORK',
true,
error,
);
}
throw new TrackDownloadError(
'Erreur inconnue lors du téléchargement',
'UNKNOWN',
false,
error,
);
}
}
/**
* Télécharge un fichier avec suivi de progression
*/
async function downloadWithProgress(
response: Response,
filename: string | undefined,
onProgress: (progress: number) => void,
): Promise<void> {
const contentLength = response.headers.get('content-length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
if (!response.body) {
throw new TrackDownloadError('Response body is null', 'UNKNOWN', false);
}
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let receivedLength = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
// Mettre à jour la progression si on connaît la taille totale
if (total > 0 && onProgress) {
const progress = Math.round((receivedLength / total) * 100);
onProgress(progress);
}
}
// Combiner tous les chunks en un seul blob
const allChunks = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
const blob = new Blob([allChunks], {
type: response.headers.get('content-type') || 'application/octet-stream',
});
await triggerDownload(blob, filename || getFilenameFromResponse(response));
} catch (error) {
throw new TrackDownloadError(
'Erreur lors du téléchargement avec progression',
'NETWORK',
true,
error,
);
}
}
/**
* Déclenche le téléchargement du fichier
*/
async function triggerDownload(blob: Blob, filename: string): Promise<void> {
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
}
/**
* Extrait le nom de fichier depuis les headers de la réponse
*/
function getFilenameFromResponse(response: Response): string {
const contentDisposition = response.headers.get('content-disposition');
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
);
if (filenameMatch && filenameMatch[1]) {
let filename = filenameMatch[1].replace(/['"]/g, '');
// Décoder les caractères encodés en URL
try {
filename = decodeURIComponent(filename);
} catch {
// Si le décodage échoue, utiliser le nom tel quel
}
return filename;
}
}
return 'track';
}