veza/veza-backend-api/internal/services/track_validation_service.go
2025-12-03 20:29:37 +01:00

261 lines
7 KiB
Go

package services
import (
"fmt"
"io"
"mime/multipart"
"strings"
"veza-backend-api/internal/utils"
)
const (
// MaxTrackSize limite maximale de taille pour un fichier audio (100MB)
MaxTrackSize = 100 * 1024 * 1024
// MinTrackDuration durée minimale d'un track en secondes (1 seconde)
MinTrackDuration = 1
// MaxTrackDuration durée maximale d'un track en secondes (3 heures)
MaxTrackDuration = 3 * 60 * 60
)
// Formats audio supportés
var AllowedFormats = []string{"audio/mpeg", "audio/flac", "audio/wav", "audio/ogg", "audio/vorbis"}
// Codecs audio supportés
var AllowedCodecs = []string{"mp3", "flac", "pcm", "vorbis", "aac"}
// TrackValidationService gère la validation des fichiers audio
type TrackValidationService struct{}
// NewTrackValidationService crée un nouveau service de validation
func NewTrackValidationService() *TrackValidationService {
return &TrackValidationService{}
}
// ValidateFormat valide le format du fichier en utilisant les magic bytes
func (s *TrackValidationService) ValidateFormat(fileHeader *multipart.FileHeader) error {
file, err := fileHeader.Open()
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
// Lire les premiers bytes pour vérifier les magic bytes
magicBytes := make([]byte, 12)
n, err := file.Read(magicBytes)
if err != nil && err != io.EOF {
return fmt.Errorf("failed to read file: %w", err)
}
if n < 4 {
return fmt.Errorf("file too small to validate format")
}
// Valider les magic bytes
if err := s.validateMagicBytes(magicBytes[:n]); err != nil {
return err
}
return nil
}
// validateMagicBytes valide les magic bytes pour les formats audio supportés
func (s *TrackValidationService) validateMagicBytes(magicBytes []byte) error {
if len(magicBytes) < 4 {
return fmt.Errorf("insufficient data for magic byte validation")
}
// MP3: ID3v2 (starts with "ID3") or MPEG frame sync (0xFF 0xFB/E/F)
if len(magicBytes) >= 3 && strings.HasPrefix(string(magicBytes[:3]), "ID3") {
return nil
}
if magicBytes[0] == 0xFF && (magicBytes[1] == 0xFB || magicBytes[1] == 0xF3 || magicBytes[1] == 0xF2 || (magicBytes[1]&0xE0) == 0xE0) {
return nil
}
// FLAC: "fLaC" (starts at offset 4 after "fLaC" stream marker)
if len(magicBytes) >= 4 && string(magicBytes[:4]) == "fLaC" {
return nil
}
// WAV: "RIFF" followed by "WAVE"
if len(magicBytes) >= 8 && string(magicBytes[:4]) == "RIFF" && string(magicBytes[8:12]) == "WAVE" {
return nil
}
if len(magicBytes) >= 4 && string(magicBytes[:4]) == "RIFF" {
// Check for WAVE in the next 4 bytes if available
if len(magicBytes) >= 8 && string(magicBytes[4:8]) == "WAVE" {
return nil
}
// If we have RIFF, check further for WAVE
additionalBytes := make([]byte, 4)
if _, err := io.ReadFull(strings.NewReader(string(magicBytes[4:])), additionalBytes); err == nil {
if string(additionalBytes) == "WAVE" {
return nil
}
}
}
// OGG: "OggS"
if len(magicBytes) >= 4 && string(magicBytes[:4]) == "OggS" {
return nil
}
// M4A/AAC: "ftyp" avec "M4A" ou "mp4"
if len(magicBytes) >= 8 {
magicStr := string(magicBytes)
if strings.Contains(magicStr, "ftyp") {
if strings.Contains(magicStr, "M4A") || strings.Contains(magicStr, "mp4") {
return nil
}
}
}
return fmt.Errorf("invalid audio file format: unsupported format or corrupted file")
}
// ValidateFileSize valide la taille du fichier
func (s *TrackValidationService) ValidateFileSize(fileHeader *multipart.FileHeader) error {
if fileHeader.Size == 0 {
return fmt.Errorf("file is empty")
}
if fileHeader.Size > MaxTrackSize {
return fmt.Errorf("file size exceeds maximum allowed size of 100MB")
}
return nil
}
// ValidateDuration valide la durée d'un track
func (s *TrackValidationService) ValidateDuration(duration int) error {
if duration < MinTrackDuration {
return fmt.Errorf("track duration is too short: minimum %d seconds required", MinTrackDuration)
}
if duration > MaxTrackDuration {
return fmt.Errorf("track duration is too long: maximum %d seconds (3 hours) allowed", MaxTrackDuration)
}
return nil
}
// ValidateCodec valide le codec audio
func (s *TrackValidationService) ValidateCodec(codec string) error {
if codec == "" {
return fmt.Errorf("codec is required")
}
codecLower := strings.ToLower(codec)
for _, allowedCodec := range AllowedCodecs {
if codecLower == strings.ToLower(allowedCodec) {
return nil
}
}
return fmt.Errorf("unsupported codec: %s. Allowed codecs: %s", codec, strings.Join(AllowedCodecs, ", "))
}
// TrackValidationResult représente le résultat d'une validation complète
type TrackValidationResult struct {
Valid bool
Format string
Codec string
Duration int
Errors []string
}
// ValidateTrackFile combine toutes les validations pour un fichier audio
func (s *TrackValidationService) ValidateTrackFile(fileHeader *multipart.FileHeader, duration int, codec string) (*TrackValidationResult, error) {
result := &TrackValidationResult{
Valid: true,
Errors: []string{},
Duration: duration,
Codec: codec,
}
// Valider la taille
if err := s.ValidateFileSize(fileHeader); err != nil {
result.Valid = false
result.Errors = append(result.Errors, err.Error())
}
// Valider le format (magic bytes)
if err := s.ValidateFormat(fileHeader); err != nil {
result.Valid = false
result.Errors = append(result.Errors, err.Error())
} else {
// Déterminer le format détecté
result.Format = s.detectFormat(fileHeader)
}
// Valider la durée si fournie
if duration > 0 {
if err := s.ValidateDuration(duration); err != nil {
result.Valid = false
result.Errors = append(result.Errors, err.Error())
}
}
// Valider le codec si fourni
if codec != "" {
if err := s.ValidateCodec(codec); err != nil {
result.Valid = false
result.Errors = append(result.Errors, err.Error())
}
}
if !result.Valid {
return result, fmt.Errorf("validation failed: %s", strings.Join(result.Errors, "; "))
}
return result, nil
}
// detectFormat détecte le format du fichier à partir des magic bytes
func (s *TrackValidationService) detectFormat(fileHeader *multipart.FileHeader) string {
file, err := fileHeader.Open()
if err != nil {
return "unknown"
}
defer file.Close()
magicBytes := make([]byte, 12)
n, err := file.Read(magicBytes)
if err != nil || n < 4 {
return "unknown"
}
// MP3
if strings.HasPrefix(string(magicBytes[:utils.Min(3, n)]), "ID3") || (magicBytes[0] == 0xFF && (magicBytes[1]&0xE0) == 0xE0) {
return "audio/mpeg"
}
// FLAC
if n >= 4 && string(magicBytes[:4]) == "fLaC" {
return "audio/flac"
}
// WAV
if n >= 4 && string(magicBytes[:4]) == "RIFF" {
return "audio/wav"
}
// OGG
if n >= 4 && string(magicBytes[:4]) == "OggS" {
return "audio/ogg"
}
// M4A/AAC
if n >= 8 {
magicStr := string(magicBytes)
if strings.Contains(magicStr, "ftyp") && (strings.Contains(magicStr, "M4A") || strings.Contains(magicStr, "mp4")) {
return "audio/m4a"
}
}
return "unknown"
}
// min est maintenant défini dans internal/utils/math.go
// Import: veza-backend-api/internal/utils