diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index ab0a53593..9840228e0 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -2498,7 +2498,19 @@ "description": "GET /api/v1/uploads/stats returns upload statistics", "owner": "backend", "estimated_hours": 2, - "status": "todo", + "status": "completed", + "completion": { + "completed_at": "2025-12-24T10:48:33Z", + "actual_hours": 1.0, + "commits": [], + "files_changed": [ + "veza-backend-api/internal/handlers/upload.go", + "veza-backend-api/internal/services/track_upload_service.go", + "veza-backend-api/internal/api/router.go" + ], + "notes": "Implemented GetUploadStats method in TrackUploadService to calculate upload statistics from tracks table. Standardized GetUploadStats handler to use RespondSuccess and RespondWithAppError. Replaced c.Get with GetUserIDUUID helper. Handler retrieves statistics including total_uploads, total_size, audio_files, image_files, and video_files. Updated UploadHandler to include TrackUploadService dependency. Updated router to pass TrackUploadService to UploadHandler.", + "issues_encountered": [] + }, "files_involved": [], "implementation_steps": [ { diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index 1e2f0011b..354756edc 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -861,7 +861,8 @@ func (r *APIRouter) setupCorePublicRoutes(router *gin.Engine) { uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } auditService := services.NewAuditService(r.db, r.logger) - uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, r.logger, r.config.MaxConcurrentUploads) + trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) + uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) v1Public.GET("/upload/limits", uploadHandler.GetUploadLimits()) v1Public.GET("/upload/validate-type", uploadHandler.ValidateFileType()) } @@ -908,10 +909,11 @@ func (r *APIRouter) setupCoreProtectedRoutes(v1 *gin.RouterGroup) { uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } auditService := services.NewAuditService(r.db, r.logger) + trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) // Handlers sessionHandler := handlers.NewSessionHandler(sessionService, auditService, r.logger) - uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, r.logger, r.config.MaxConcurrentUploads) + uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) auditHandler := handlers.NewAuditHandler(auditService, r.logger) // Routes de session diff --git a/veza-backend-api/internal/handlers/upload.go b/veza-backend-api/internal/handlers/upload.go index 4d09644f5..0faf9ca12 100644 --- a/veza-backend-api/internal/handlers/upload.go +++ b/veza-backend-api/internal/handlers/upload.go @@ -38,10 +38,11 @@ type UploadResponse struct { // UploadHandler gère les uploads de fichiers type UploadHandler struct { - uploadValidator *services.UploadValidator - auditService *services.AuditService - logger *zap.Logger - uploadSemaphore chan struct{} // MOD-P2-005: Sémaphore pour limiter uploads simultanés + uploadValidator *services.UploadValidator + auditService *services.AuditService + trackUploadService *services.TrackUploadService + logger *zap.Logger + uploadSemaphore chan struct{} // MOD-P2-005: Sémaphore pour limiter uploads simultanés } // NewUploadHandler crée un nouveau handler d'upload @@ -49,6 +50,7 @@ type UploadHandler struct { func NewUploadHandler( uploadValidator *services.UploadValidator, auditService *services.AuditService, + trackUploadService *services.TrackUploadService, logger *zap.Logger, maxConcurrentUploads int, ) *UploadHandler { @@ -56,10 +58,11 @@ func NewUploadHandler( maxConcurrentUploads = 10 // Valeur par défaut } return &UploadHandler{ - uploadValidator: uploadValidator, - auditService: auditService, - logger: logger, - uploadSemaphore: make(chan struct{}, maxConcurrentUploads), // MOD-P2-005: Sémaphore bufferisé + uploadValidator: uploadValidator, + auditService: auditService, + trackUploadService: trackUploadService, + logger: logger, + uploadSemaphore: make(chan struct{}, maxConcurrentUploads), // MOD-P2-005: Sémaphore bufferisé } } @@ -315,33 +318,34 @@ func (uh *UploadHandler) DeleteUpload() gin.HandlerFunc { } // GetUploadStats récupère les statistiques d'upload +// GET /api/v1/uploads/stats +// BE-API-032: Implement upload stats endpoint func (uh *UploadHandler) GetUploadStats() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte - userIDInterface, exists := c.Get("user_id") - if !exists { - // MOD-P2-003: Utiliser AppError au lieu de gin.H - RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated")) - return - } - - userID, ok := userIDInterface.(uuid.UUID) + userID, ok := GetUserIDUUID(c) if !ok { - // MOD-P2-003: Utiliser AppError au lieu de gin.H - RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type")) - return + return // Erreur déjà envoyée par GetUserIDUUID } // Récupérer les statistiques depuis la base de données - // Note: Dans un vrai environnement, il faudrait interroger la DB - stats := map[string]interface{}{ - "total_uploads": 0, - "total_size": 0, - "audio_files": 0, - "image_files": 0, - "video_files": 0, + if uh.trackUploadService == nil { + uh.logger.Error("TrackUploadService not available") + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Upload stats service not available", nil)) + return } + stats, err := uh.trackUploadService.GetUploadStats(c.Request.Context(), userID) + if err != nil { + uh.logger.Error("Failed to get upload stats", + zap.Error(err), + zap.String("user_id", userID.String()), + ) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get upload stats", err)) + return + } + + // BE-API-032: Standardize response format RespondSuccess(c, http.StatusOK, gin.H{ "user_id": userID, "stats": stats, diff --git a/veza-backend-api/internal/services/track_upload_service.go b/veza-backend-api/internal/services/track_upload_service.go index 327b32edb..8abe5622a 100644 --- a/veza-backend-api/internal/services/track_upload_service.go +++ b/veza-backend-api/internal/services/track_upload_service.go @@ -4,10 +4,11 @@ import ( "context" "fmt" + "veza-backend-api/internal/models" + "github.com/google/uuid" // Added import for uuid "go.uber.org/zap" "gorm.io/gorm" - "veza-backend-api/internal/models" ) // TrackUploadService gère le suivi de progression des uploads de tracks @@ -87,3 +88,67 @@ func (s *TrackUploadService) calculateProgress(status models.TrackStatus) int { return 0 } } + +// GetUploadStats récupère les statistiques d'upload pour un utilisateur +// BE-API-032: Implement upload stats endpoint +func (s *TrackUploadService) GetUploadStats(ctx context.Context, userID uuid.UUID) (map[string]interface{}, error) { + stats := map[string]interface{}{ + "total_uploads": int64(0), + "total_size": int64(0), + "audio_files": int64(0), + "image_files": int64(0), + "video_files": int64(0), + } + + // Compter le nombre total d'uploads (tracks) pour l'utilisateur + var totalUploads int64 + if err := s.db.WithContext(ctx).Model(&models.Track{}). + Where("creator_id = ?", userID). + Count(&totalUploads).Error; err != nil { + return nil, fmt.Errorf("failed to count total uploads: %w", err) + } + stats["total_uploads"] = totalUploads + + // Calculer la taille totale des fichiers + var totalSize int64 + if err := s.db.WithContext(ctx).Model(&models.Track{}). + Where("creator_id = ?", userID). + Select("COALESCE(SUM(file_size), 0)"). + Scan(&totalSize).Error; err != nil { + return nil, fmt.Errorf("failed to calculate total size: %w", err) + } + stats["total_size"] = totalSize + + // Compter les fichiers audio (mp3, flac, wav, etc.) + var audioFiles int64 + if err := s.db.WithContext(ctx).Model(&models.Track{}). + Where("creator_id = ? AND format IN (?)", userID, []string{"mp3", "flac", "wav", "aac", "ogg", "m4a"}). + Count(&audioFiles).Error; err != nil { + return nil, fmt.Errorf("failed to count audio files: %w", err) + } + stats["audio_files"] = audioFiles + + // Compter les fichiers image (jpg, png, gif, etc.) + // Note: Les images sont généralement dans cover_art_path, mais on peut aussi chercher dans les tracks + // Pour l'instant, on se concentre sur les tracks audio + var imageFiles int64 + if err := s.db.WithContext(ctx).Model(&models.Track{}). + Where("creator_id = ? AND format IN (?)", userID, []string{"jpg", "jpeg", "png", "gif", "webp"}). + Count(&imageFiles).Error; err != nil { + // Si la requête échoue (format non supporté pour images), on laisse à 0 + s.logger.Debug("Image files count query failed, setting to 0", zap.Error(err)) + } + stats["image_files"] = imageFiles + + // Compter les fichiers vidéo (mp4, avi, etc.) + var videoFiles int64 + if err := s.db.WithContext(ctx).Model(&models.Track{}). + Where("creator_id = ? AND format IN (?)", userID, []string{"mp4", "avi", "mov", "mkv", "webm"}). + Count(&videoFiles).Error; err != nil { + // Si la requête échoue (format non supporté pour vidéos), on laisse à 0 + s.logger.Debug("Video files count query failed, setting to 0", zap.Error(err)) + } + stats["video_files"] = videoFiles + + return stats, nil +}