veza/veza-backend-api/internal/handlers/upload.go

476 lines
13 KiB
Go

package handlers
import (
"fmt"
"net/http"
"time"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// UploadRequest requête pour upload de fichier
type UploadRequest struct {
TrackID uuid.UUID `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
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"`
}
// UploadHandler gère les uploads de fichiers
type UploadHandler struct {
uploadValidator *services.UploadValidator
auditService *services.AuditService
logger *zap.Logger
}
// NewUploadHandler crée un nouveau handler d'upload
func NewUploadHandler(
uploadValidator *services.UploadValidator,
auditService *services.AuditService,
logger *zap.Logger,
) *UploadHandler {
return &UploadHandler{
uploadValidator: uploadValidator,
auditService: auditService,
logger: logger,
}
}
// UploadFile gère l'upload d'un fichier
func (uh *UploadHandler) UploadFile() gin.HandlerFunc {
return func(c *gin.Context) {
// Récupérer l'ID utilisateur depuis le contexte
userIDInterface, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
// Parser la requête multipart
var req UploadRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Récupérer le fichier
fileHeader, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
return
}
// Valider le fichier
validationResult, err := uh.uploadValidator.ValidateFile(fileHeader, req.FileType)
if err != nil {
uh.logger.Error("File validation failed",
zap.Error(err),
zap.String("user_id", userID.String()),
zap.String("file_name", fileHeader.Filename),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "File validation failed"})
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),
)
c.JSON(http.StatusBadRequest, gin.H{"error": validationResult.Error})
return
}
// Vérifier si le fichier a été mis en quarantaine
if validationResult.Quarantined {
uh.logger.Warn("File quarantined",
zap.String("user_id", userID.String()),
zap.String("file_name", fileHeader.Filename),
zap.String("reason", validationResult.Error),
)
c.JSON(http.StatusBadRequest, gin.H{
"error": "File rejected for security reasons",
"details": validationResult.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,
req.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),
)
// Retourner la réponse
response := &UploadResponse{
ID: uploadID,
TrackID: req.TrackID,
FileName: fileHeader.Filename,
FileSize: validationResult.FileSize,
FileType: validationResult.FileType,
Checksum: validationResult.Checksum,
Status: "uploaded",
CreatedAt: time.Now(),
}
RespondSuccess(c, http.StatusCreated, gin.H{
"message": "File uploaded successfully",
"data": 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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
uploadIDStr := c.Param("id")
uploadID, err := uuid.Parse(uploadIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "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
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
// 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,
}
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 == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
// Parser le formulaire multipart
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid multipart form"})
return
}
files := form.File["files"]
if len(files) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "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
}
// Valider le fichier
validationResult, err := uh.uploadValidator.ValidateFile(fileHeader, fileType)
if err != nil {
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),
},
})
}
}