- Tests complets pour frontend_log_handler.go (12 tests)
- Tests couvrent NewFrontendLogHandler et ReceiveLog
- Tests pour tous les niveaux de log (DEBUG, INFO, WARN, ERROR)
- Tests pour gestion des erreurs et validation JSON
- Couverture actuelle: 30.6% (objectif: 80%)
Files: veza-backend-api/internal/handlers/frontend_log_handler_test.go
VEZA_ROADMAP.json
Hours: 16 estimated, 23 actual
627 lines
20 KiB
Go
627 lines
20 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"veza-backend-api/internal/services"
|
|
"veza-backend-api/internal/upload"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// UploadRequest requête pour upload de fichier
|
|
// DEPRECATED: Use upload.StandardUploadRequest instead
|
|
// INT-015: Kept for backward compatibility during migration
|
|
type UploadRequest struct {
|
|
TrackID string `form:"track_id" binding:"required"`
|
|
FileType string `form:"file_type" binding:"required,oneof=audio image video"`
|
|
Title string `form:"title" binding:"required,min=1,max=255"`
|
|
Artist string `form:"artist" binding:"required,min=1,max=255"`
|
|
Duration int `form:"duration" binding:"min=0"`
|
|
Metadata string `form:"metadata"`
|
|
}
|
|
|
|
// UploadResponse réponse pour upload
|
|
// DEPRECATED: Use upload.StandardUploadResponse instead
|
|
// INT-015: Kept for backward compatibility during migration
|
|
type UploadResponse struct {
|
|
ID uuid.UUID `json:"id"`
|
|
TrackID uuid.UUID `json:"track_id"`
|
|
FileName string `json:"file_name"`
|
|
FileSize int64 `json:"file_size"`
|
|
FileType string `json:"file_type"`
|
|
Checksum string `json:"checksum"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// UploadValidatorInterface définit les méthodes nécessaires pour UploadValidator
|
|
type UploadValidatorInterface interface {
|
|
ValidateFile(ctx context.Context, fileHeader *multipart.FileHeader, fileType string) (*services.ValidationResult, error)
|
|
GetFileTypeFromPath(filename string) string
|
|
}
|
|
|
|
// UploadAuditServiceInterface définit les méthodes nécessaires pour AuditService dans le contexte d'upload
|
|
type UploadAuditServiceInterface interface {
|
|
LogUpload(ctx context.Context, userID uuid.UUID, resourceID uuid.UUID, fileName string, fileSize int64, ipAddress, userAgent string) error
|
|
LogDeletion(ctx context.Context, userID uuid.UUID, resource string, resourceID uuid.UUID, ipAddress, userAgent string) error
|
|
}
|
|
|
|
// TrackUploadServiceInterface définit les méthodes nécessaires pour TrackUploadService
|
|
type TrackUploadServiceInterface interface {
|
|
GetUploadStats(ctx context.Context, userID uuid.UUID) (map[string]interface{}, error)
|
|
}
|
|
|
|
// UploadHandler gère les uploads de fichiers
|
|
type UploadHandler struct {
|
|
uploadValidator UploadValidatorInterface
|
|
auditService UploadAuditServiceInterface
|
|
trackUploadService TrackUploadServiceInterface
|
|
logger *zap.Logger
|
|
uploadSemaphore chan struct{} // MOD-P2-005: Sémaphore pour limiter uploads simultanés
|
|
}
|
|
|
|
// NewUploadHandler crée un nouveau handler d'upload
|
|
// MOD-P2-005: maxConcurrentUploads définit la limite d'uploads simultanés (backpressure)
|
|
func NewUploadHandler(
|
|
uploadValidator *services.UploadValidator,
|
|
auditService *services.AuditService,
|
|
trackUploadService *services.TrackUploadService,
|
|
logger *zap.Logger,
|
|
maxConcurrentUploads int,
|
|
) *UploadHandler {
|
|
if maxConcurrentUploads <= 0 {
|
|
maxConcurrentUploads = 10 // Valeur par défaut
|
|
}
|
|
return &UploadHandler{
|
|
uploadValidator: uploadValidator,
|
|
auditService: auditService,
|
|
trackUploadService: trackUploadService,
|
|
logger: logger,
|
|
uploadSemaphore: make(chan struct{}, maxConcurrentUploads), // MOD-P2-005: Sémaphore bufferisé
|
|
}
|
|
}
|
|
|
|
// NewUploadHandlerWithInterface crée un nouveau handler d'upload avec des interfaces (pour les tests)
|
|
func NewUploadHandlerWithInterface(
|
|
uploadValidator UploadValidatorInterface,
|
|
auditService UploadAuditServiceInterface,
|
|
trackUploadService TrackUploadServiceInterface,
|
|
logger *zap.Logger,
|
|
maxConcurrentUploads int,
|
|
) *UploadHandler {
|
|
if maxConcurrentUploads <= 0 {
|
|
maxConcurrentUploads = 10 // Valeur par défaut
|
|
}
|
|
return &UploadHandler{
|
|
uploadValidator: uploadValidator,
|
|
auditService: auditService,
|
|
trackUploadService: trackUploadService,
|
|
logger: logger,
|
|
uploadSemaphore: make(chan struct{}, maxConcurrentUploads),
|
|
}
|
|
}
|
|
|
|
// UploadFile gère l'upload d'un fichier
|
|
// MOD-P2-005: Utilise un sémaphore pour limiter les uploads simultanés (backpressure)
|
|
func (uh *UploadHandler) UploadFile() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// MOD-P2-005: Acquérir le sémaphore (bloque si limite atteinte)
|
|
select {
|
|
case uh.uploadSemaphore <- struct{}{}:
|
|
// Sémaphore acquis, continuer
|
|
defer func() { <-uh.uploadSemaphore }() // Libérer le sémaphore à la fin
|
|
case <-c.Request.Context().Done():
|
|
// Contexte annulé (timeout, etc.)
|
|
return
|
|
default:
|
|
// Sémaphore plein, retourner 503 Service Unavailable
|
|
uh.logger.Warn("Upload rejected: too many concurrent uploads",
|
|
zap.String("user_id", c.GetString("user_id")),
|
|
zap.String("ip", c.ClientIP()),
|
|
)
|
|
c.Header("Retry-After", "30") // Suggérer de réessayer après 30 secondes
|
|
// INT-015: Use standardized error format
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"success": false,
|
|
"error": gin.H{
|
|
"code": upload.ErrorCodeTooManyConcurrent,
|
|
"message": "Too many concurrent uploads. Please try again later.",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
if !ok {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
// Parser la requête multipart
|
|
var req UploadRequest
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, err.Error()))
|
|
return
|
|
}
|
|
|
|
trackID, err := uuid.Parse(req.TrackID)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid track ID"))
|
|
return
|
|
}
|
|
|
|
// Récupérer le fichier
|
|
fileHeader, err := c.FormFile("file")
|
|
if err != nil {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "No file provided"))
|
|
return
|
|
}
|
|
|
|
// MOD-P1-001: Valider le fichier AVANT toute persistance (scan ClamAV inclus)
|
|
validationResult, err := uh.uploadValidator.ValidateFile(c.Request.Context(), fileHeader, req.FileType)
|
|
if err != nil {
|
|
// MOD-P1-001-REFINEMENT: Détecter erreur ClamAV unavailable et retourner 503
|
|
if strings.Contains(err.Error(), "clamav_unavailable") {
|
|
uh.logger.Warn("Upload rejected: ClamAV unavailable",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("file_name", fileHeader.Filename),
|
|
zap.Error(err),
|
|
)
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "Virus scanning service is temporarily unavailable",
|
|
"message": "Uploads are disabled for security reasons until the scanning service is restored",
|
|
"code": "SERVICE_UNAVAILABLE",
|
|
})
|
|
return
|
|
}
|
|
uh.logger.Error("File validation failed",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("file_name", fileHeader.Filename),
|
|
)
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "File validation failed"))
|
|
return
|
|
}
|
|
|
|
// MOD-P1-001: Détecter virus détecté (code 422) vs autres erreurs
|
|
if validationResult.Quarantined || (err != nil && strings.Contains(err.Error(), "clamav_infected")) {
|
|
uh.logger.Warn("File rejected: virus detected",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("file_name", fileHeader.Filename),
|
|
zap.String("reason", validationResult.Error),
|
|
)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{
|
|
"error": "File rejected: virus detected",
|
|
"details": validationResult.Error,
|
|
"code": "VIRUS_DETECTED",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Vérifier si le fichier est valide
|
|
if !validationResult.Valid {
|
|
uh.logger.Warn("Invalid file uploaded",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("file_name", fileHeader.Filename),
|
|
zap.String("error", validationResult.Error),
|
|
)
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, validationResult.Error))
|
|
return
|
|
}
|
|
|
|
// MOD-P1-001: Détecter erreur de scan ClamAV (timeout, connexion, etc.)
|
|
if err != nil && strings.Contains(err.Error(), "clamav_scan_error") {
|
|
uh.logger.Error("Upload rejected: ClamAV scan error",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("file_name", fileHeader.Filename),
|
|
zap.Error(err),
|
|
)
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "Virus scan failed",
|
|
"message": "Unable to complete virus scan. Upload rejected for security.",
|
|
"code": "SCAN_ERROR",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Créer l'enregistrement en base de données
|
|
// Note: Dans un vrai environnement, il faudrait sauvegarder le fichier
|
|
// et créer l'enregistrement dans la table tracks
|
|
uploadID := uuid.New()
|
|
|
|
// Log l'upload dans l'audit
|
|
err = uh.auditService.LogUpload(
|
|
c.Request.Context(),
|
|
userID,
|
|
trackID,
|
|
fileHeader.Filename,
|
|
validationResult.FileSize,
|
|
c.ClientIP(),
|
|
c.GetHeader("User-Agent"),
|
|
)
|
|
if err != nil {
|
|
uh.logger.Error("Failed to log upload audit",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
// Ne pas faire échouer l'upload pour une erreur d'audit
|
|
}
|
|
|
|
uh.logger.Info("File uploaded successfully",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("upload_id", uploadID.String()),
|
|
zap.String("file_name", fileHeader.Filename),
|
|
zap.Int64("file_size", validationResult.FileSize),
|
|
zap.String("file_type", validationResult.FileType),
|
|
)
|
|
|
|
// INT-015: Return standardized upload response
|
|
now := time.Now()
|
|
virusScanResult := "clean"
|
|
response := &upload.StandardUploadResponse{
|
|
ID: uploadID,
|
|
TrackID: &trackID,
|
|
FileName: fileHeader.Filename,
|
|
FileSize: validationResult.FileSize,
|
|
FileType: validationResult.FileType,
|
|
MimeType: fileHeader.Header.Get("Content-Type"),
|
|
Checksum: validationResult.Checksum,
|
|
Status: upload.UploadStatusCompleted,
|
|
Progress: 100,
|
|
BytesUploaded: validationResult.FileSize,
|
|
StoragePath: "", // Will be set by storage service
|
|
StorageProvider: "s3",
|
|
IsProcessed: false,
|
|
VirusScanned: true,
|
|
VirusScanResult: &virusScanResult,
|
|
VirusScannedAt: &now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, response)
|
|
}
|
|
}
|
|
|
|
// GetUploadStatus récupère le statut d'un upload
|
|
func (uh *UploadHandler) GetUploadStatus() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uploadIDStr := c.Param("id")
|
|
uploadID, err := uuid.Parse(uploadIDStr)
|
|
if err != nil {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid upload ID"))
|
|
return
|
|
}
|
|
|
|
// Récupérer le statut depuis la base de données
|
|
// Note: Dans un vrai environnement, il faudrait interroger la DB
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"id": uploadID,
|
|
"status": "completed",
|
|
"progress": 100,
|
|
})
|
|
}
|
|
}
|
|
|
|
// DeleteUpload supprime un upload
|
|
func (uh *UploadHandler) DeleteUpload() 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)
|
|
if !ok {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
uploadIDStr := c.Param("id")
|
|
uploadID, err := uuid.Parse(uploadIDStr)
|
|
if err != nil {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid upload ID"))
|
|
return
|
|
}
|
|
|
|
// Log la suppression dans l'audit
|
|
err = uh.auditService.LogDeletion(
|
|
c.Request.Context(),
|
|
userID,
|
|
"upload",
|
|
uploadID,
|
|
c.ClientIP(),
|
|
c.GetHeader("User-Agent"),
|
|
)
|
|
if err != nil {
|
|
uh.logger.Error("Failed to log deletion audit",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
}
|
|
|
|
uh.logger.Info("Upload deleted",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("upload_id", uploadID.String()),
|
|
)
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"message": "Upload deleted successfully",
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
// Récupérer les statistiques depuis la base de données
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ValidateFileType valide le type de fichier
|
|
func (uh *UploadHandler) ValidateFileType() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
fileType := c.Query("type")
|
|
if fileType == "" {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "File type parameter required"))
|
|
return
|
|
}
|
|
|
|
// Vérifier si le type est supporté
|
|
supportedTypes := []string{"audio", "image", "video"}
|
|
isSupported := false
|
|
for _, supportedType := range supportedTypes {
|
|
if fileType == supportedType {
|
|
isSupported = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isSupported {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Unsupported file type",
|
|
"supported_types": supportedTypes,
|
|
})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"type": fileType,
|
|
"supported": true,
|
|
"supported_types": supportedTypes,
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetUploadLimits récupère les limites d'upload
|
|
func (uh *UploadHandler) GetUploadLimits() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
limits := map[string]interface{}{
|
|
"audio": map[string]interface{}{
|
|
"max_size": "100MB",
|
|
"max_size_bytes": 100 * 1024 * 1024,
|
|
"allowed_types": []string{
|
|
"audio/mpeg",
|
|
"audio/mp3",
|
|
"audio/wav",
|
|
"audio/flac",
|
|
"audio/aac",
|
|
"audio/ogg",
|
|
"audio/m4a",
|
|
},
|
|
},
|
|
"image": map[string]interface{}{
|
|
"max_size": "10MB",
|
|
"max_size_bytes": 10 * 1024 * 1024,
|
|
"allowed_types": []string{
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/gif",
|
|
"image/webp",
|
|
"image/svg+xml",
|
|
},
|
|
},
|
|
"video": map[string]interface{}{
|
|
"max_size": "500MB",
|
|
"max_size_bytes": 500 * 1024 * 1024,
|
|
"allowed_types": []string{
|
|
"video/mp4",
|
|
"video/webm",
|
|
"video/ogg",
|
|
"video/avi",
|
|
},
|
|
},
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"limits": limits,
|
|
})
|
|
}
|
|
}
|
|
|
|
// UploadProgress gère le suivi de progression d'upload
|
|
func (uh *UploadHandler) UploadProgress() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uploadIDStr := c.Param("id")
|
|
uploadID, err := uuid.Parse(uploadIDStr)
|
|
if err != nil {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid upload ID"))
|
|
return
|
|
}
|
|
|
|
// Récupérer la progression depuis la base de données
|
|
// Note: Dans un vrai environnement, il faudrait interroger la DB
|
|
progress := map[string]interface{}{
|
|
"upload_id": uploadID,
|
|
"status": "completed",
|
|
"progress": 100,
|
|
"bytes_uploaded": 0,
|
|
"total_bytes": 0,
|
|
"estimated_time_remaining": 0,
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, progress)
|
|
}
|
|
}
|
|
|
|
// BatchUpload gère les uploads multiples
|
|
func (uh *UploadHandler) BatchUpload() 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)
|
|
if !ok {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
// Parser le formulaire multipart
|
|
form, err := c.MultipartForm()
|
|
if err != nil {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid multipart form"))
|
|
return
|
|
}
|
|
|
|
files := form.File["files"]
|
|
if len(files) == 0 {
|
|
// MOD-P2-003: Utiliser AppError au lieu de gin.H
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "No files provided"))
|
|
return
|
|
}
|
|
|
|
// Limiter le nombre de fichiers par batch
|
|
maxFiles := 10
|
|
if len(files) > maxFiles {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("Too many files. Maximum %d files per batch", maxFiles),
|
|
})
|
|
return
|
|
}
|
|
|
|
var results []map[string]interface{}
|
|
var errors []string
|
|
|
|
for i, fileHeader := range files {
|
|
// Déterminer le type de fichier à partir de l'extension
|
|
fileType := uh.uploadValidator.GetFileTypeFromPath(fileHeader.Filename)
|
|
if fileType == "unknown" {
|
|
errors = append(errors, fmt.Sprintf("File %d (%s): Unknown file type", i+1, fileHeader.Filename))
|
|
continue
|
|
}
|
|
|
|
// MOD-P1-001: Valider le fichier AVANT toute persistance
|
|
validationResult, err := uh.uploadValidator.ValidateFile(c.Request.Context(), fileHeader, fileType)
|
|
if err != nil {
|
|
// MOD-P1-001-REFINEMENT: Détecter erreur ClamAV unavailable
|
|
if strings.Contains(err.Error(), "clamav_unavailable") {
|
|
errors = append(errors, fmt.Sprintf("File %d (%s): Virus scanning service unavailable", i+1, fileHeader.Filename))
|
|
} else {
|
|
errors = append(errors, fmt.Sprintf("File %d (%s): Validation error", i+1, fileHeader.Filename))
|
|
}
|
|
continue
|
|
}
|
|
|
|
if !validationResult.Valid {
|
|
errors = append(errors, fmt.Sprintf("File %d (%s): %s", i+1, fileHeader.Filename, validationResult.Error))
|
|
continue
|
|
}
|
|
|
|
// Créer le résultat
|
|
result := map[string]interface{}{
|
|
"index": i + 1,
|
|
"file_name": fileHeader.Filename,
|
|
"file_size": validationResult.FileSize,
|
|
"file_type": validationResult.FileType,
|
|
"checksum": validationResult.Checksum,
|
|
"status": "validated",
|
|
"upload_id": uuid.New(),
|
|
}
|
|
|
|
results = append(results, result)
|
|
}
|
|
|
|
uh.logger.Info("Batch upload processed",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("total_files", len(files)),
|
|
zap.Int("successful", len(results)),
|
|
zap.Int("errors", len(errors)),
|
|
)
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"message": "Batch upload processed",
|
|
"results": results,
|
|
"errors": errors,
|
|
"summary": map[string]interface{}{
|
|
"total_files": len(files),
|
|
"successful": len(results),
|
|
"errors": len(errors),
|
|
},
|
|
})
|
|
}
|
|
}
|