[BE-API-032] be-api: Implement upload stats endpoint

- Added GetUploadStats method in TrackUploadService to calculate statistics from tracks table
- Standardized GetUploadStats handler to use RespondSuccess/RespondWithAppError
- Replaced c.Get with GetUserIDUUID helper
- Handler retrieves statistics: total_uploads, total_size, audio_files, image_files, video_files
- Updated UploadHandler to include TrackUploadService dependency
- Updated router to pass TrackUploadService to UploadHandler

Phase: PHASE-2
Priority: P2
Progress: 39/267 (14.6%)
This commit is contained in:
senke 2025-12-24 11:52:49 +01:00
parent 1a48becaa1
commit f14966ceb2
4 changed files with 113 additions and 30 deletions

View file

@ -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": [
{

View file

@ -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

View file

@ -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,

View file

@ -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
}