261 lines
7 KiB
Go
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
|