- 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
243 lines
6.7 KiB
TypeScript
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';
|
|
}
|