package services import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "veza-backend-api/internal/models" "go.uber.org/zap" "gorm.io/gorm" ) // HLSCleanupService gère le nettoyage des segments HLS obsolètes type HLSCleanupService struct { db *gorm.DB outputDir string logger *zap.Logger } // NewHLSCleanupService crée un nouveau service de cleanup HLS func NewHLSCleanupService(db *gorm.DB, outputDir string, logger *zap.Logger) *HLSCleanupService { if logger == nil { logger = zap.NewNop() } return &HLSCleanupService{ db: db, outputDir: outputDir, logger: logger, } } // CleanupDeletedTracks nettoie les segments HLS des tracks supprimés // MIGRATION UUID: Completée. TrackID et StreamID en UUID. func (s *HLSCleanupService) CleanupDeletedTracks(ctx context.Context) (int, error) { var streams []models.HLSStream if err := s.db.WithContext(ctx).Find(&streams).Error; err != nil { return 0, fmt.Errorf("failed to fetch streams: %w", err) } cleanedCount := 0 for _, stream := range streams { var track models.Track if err := s.db.WithContext(ctx).First(&track, "id = ?", stream.TrackID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // Track deleted, cleanup segments s.logger.Info("Cleaning up segments for deleted track", zap.String("stream_id", stream.ID.String()), zap.String("track_id", stream.TrackID.String())) if err := s.cleanupStreamFiles(stream); err != nil { s.logger.Error("Failed to cleanup stream files", zap.String("stream_id", stream.ID.String()), zap.Error(err)) // Continue avec les autres streams même en cas d'erreur } if err := s.db.WithContext(ctx).Delete(&stream).Error; err != nil { s.logger.Error("Failed to delete stream record", zap.String("stream_id", stream.ID.String()), zap.Error(err)) // Continue avec les autres streams } else { cleanedCount++ } } else { s.logger.Error("Failed to check track existence", zap.String("stream_id", stream.ID.String()), zap.String("track_id", stream.TrackID.String()), zap.Error(err)) } } } s.logger.Info("Cleanup deleted tracks completed", zap.Int("cleaned_count", cleanedCount)) return cleanedCount, nil } // CleanupOrphanedSegments nettoie les segments HLS qui n'ont pas de stream associé dans la base de données func (s *HLSCleanupService) CleanupOrphanedSegments(ctx context.Context) (int, error) { // Récupérer tous les streams valides var streams []models.HLSStream if err := s.db.WithContext(ctx).Find(&streams).Error; err != nil { return 0, fmt.Errorf("failed to fetch streams: %w", err) } // Créer un map des répertoires de streams valides validDirs := make(map[string]bool) for _, stream := range streams { // Construire le chemin du répertoire du stream trackDir := filepath.Join(s.outputDir, fmt.Sprintf("track_%s", stream.TrackID)) validDirs[trackDir] = true } // Parcourir le répertoire de sortie HLS cleanedCount := 0 err := filepath.Walk(s.outputDir, func(path string, info os.FileInfo, err error) error { if err != nil { // Ignorer les erreurs de lecture de répertoire return nil } // Vérifier si c'est un répertoire de track (format: track_XXX) if !info.IsDir() { return nil } // Obtenir le répertoire parent pour vérifier si c'est un track_XXX dir := path base := filepath.Base(dir) if !strings.HasPrefix(base, "track_") { return nil } // Vérifier si ce répertoire est dans la liste des répertoires valides if !validDirs[dir] { s.logger.Info("Found orphaned segment directory", zap.String("path", dir)) // Supprimer le répertoire orphelin if err := os.RemoveAll(dir); err != nil { s.logger.Error("Failed to remove orphaned directory", zap.String("path", dir), zap.Error(err)) return nil // Continue avec les autres répertoires } cleanedCount++ } return nil }) if err != nil { return cleanedCount, fmt.Errorf("failed to walk output directory: %w", err) } s.logger.Info("Cleanup orphaned segments completed", zap.Int("cleaned_count", cleanedCount)) return cleanedCount, nil } // cleanupStreamFiles supprime les fichiers d'un stream func (s *HLSCleanupService) cleanupStreamFiles(stream models.HLSStream) error { // Construire le chemin du répertoire du track trackDir := filepath.Join(s.outputDir, fmt.Sprintf("track_%s", stream.TrackID)) // Vérifier que le chemin est sécurisé (pas de directory traversal) absTrackDir, err := filepath.Abs(trackDir) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } absOutputDir, err := filepath.Abs(s.outputDir) if err != nil { return fmt.Errorf("failed to get absolute output dir: %w", err) } // Vérifier que le répertoire est bien dans outputDir if !strings.HasPrefix(absTrackDir, absOutputDir) { return fmt.Errorf("invalid track directory path: %s", trackDir) } // Supprimer le répertoire et tous ses contenus if err := os.RemoveAll(trackDir); err != nil { return fmt.Errorf("failed to remove track directory: %w", err) } s.logger.Debug("Cleaned up stream files", zap.String("track_id", stream.TrackID.String()), zap.String("track_dir", trackDir)) return nil } // CleanupAll exécute tous les nettoyages func (s *HLSCleanupService) CleanupAll(ctx context.Context) error { s.logger.Info("Starting HLS cleanup") // Nettoyer les tracks supprimés deletedCount, err := s.CleanupDeletedTracks(ctx) if err != nil { s.logger.Error("Failed to cleanup deleted tracks", zap.Error(err)) return fmt.Errorf("failed to cleanup deleted tracks: %w", err) } // Nettoyer les segments orphelins orphanedCount, err := s.CleanupOrphanedSegments(ctx) if err != nil { s.logger.Error("Failed to cleanup orphaned segments", zap.Error(err)) return fmt.Errorf("failed to cleanup orphaned segments: %w", err) } s.logger.Info("HLS cleanup completed", zap.Int("deleted_tracks_cleaned", deletedCount), zap.Int("orphaned_segments_cleaned", orphanedCount)) return nil }