veza/veza-backend-api/internal/services/hls_cleanup_service.go

203 lines
6 KiB
Go

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
}