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