package services import ( "context" "errors" "fmt" "github.com/google/uuid" "io" "os" "path/filepath" "go.uber.org/zap" "gorm.io/gorm" "veza-backend-api/internal/models" ) var ( // ErrVersionNotFound est retourné quand une version n'est pas trouvée ErrVersionNotFound = errors.New("version not found") ) // TrackVersionService gère le versioning de tracks type TrackVersionService struct { db *gorm.DB logger *zap.Logger uploadDir string } // NewTrackVersionService crée un nouveau service de versioning de tracks func NewTrackVersionService(db *gorm.DB, logger *zap.Logger, uploadDir string) *TrackVersionService { if logger == nil { logger = zap.NewNop() } return &TrackVersionService{ db: db, logger: logger, uploadDir: uploadDir, } } // CreateVersionParams représente les paramètres pour créer une nouvelle version type CreateVersionParams struct { FilePath string FileSize int64 Changelog string } // CreateVersion crée une nouvelle version d'un track // MIGRATION UUID: Completée. UserID et TrackID en UUID. func (s *TrackVersionService) CreateVersion(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, params CreateVersionParams) (*models.TrackVersion, error) { // Vérifier que le track existe et appartient à l'utilisateur var track models.Track if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTrackNotFound } return nil, fmt.Errorf("failed to get track: %w", err) } if track.UserID != userID { return nil, ErrForbidden } // Trouver le prochain numéro de version var maxVersion int if err := s.db.WithContext(ctx).Model(&models.TrackVersion{}). Where("track_id = ?", trackID). Select("COALESCE(MAX(version_number), 0)"). Scan(&maxVersion).Error; err != nil { return nil, fmt.Errorf("failed to get max version number: %w", err) } nextVersion := maxVersion + 1 // Créer la nouvelle version version := &models.TrackVersion{ TrackID: trackID, VersionNumber: nextVersion, FilePath: params.FilePath, FileSize: params.FileSize, Changelog: params.Changelog, } if err := s.db.WithContext(ctx).Create(version).Error; err != nil { return nil, fmt.Errorf("failed to create version: %w", err) } s.logger.Info("Track version created", zap.String("track_id", trackID.String()), zap.String("version_id", version.ID.String()), zap.Int("version_number", nextVersion), zap.String("user_id", userID.String()), ) return version, nil } // GetVersion récupère une version spécifique d'un track func (s *TrackVersionService) GetVersion(ctx context.Context, trackID uuid.UUID, versionID uuid.UUID) (*models.TrackVersion, error) { var version models.TrackVersion if err := s.db.WithContext(ctx). Where("id = ? AND track_id = ?", versionID, trackID). First(&version).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrVersionNotFound } return nil, fmt.Errorf("failed to get version: %w", err) } return &version, nil } // GetVersionByNumber récupère une version par son numéro func (s *TrackVersionService) GetVersionByNumber(ctx context.Context, trackID uuid.UUID, versionNumber int) (*models.TrackVersion, error) { var version models.TrackVersion if err := s.db.WithContext(ctx). Where("track_id = ? AND version_number = ?", trackID, versionNumber). First(&version).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrVersionNotFound } return nil, fmt.Errorf("failed to get version: %w", err) } return &version, nil } // ListVersions récupère toutes les versions d'un track func (s *TrackVersionService) ListVersions(ctx context.Context, trackID uuid.UUID) ([]models.TrackVersion, error) { var versions []models.TrackVersion if err := s.db.WithContext(ctx). Where("track_id = ?", trackID). Order("version_number DESC"). Find(&versions).Error; err != nil { return nil, fmt.Errorf("failed to list versions: %w", err) } return versions, nil } // RestoreVersion restaure une version spécifique (copie le fichier de la version vers le track actuel) // MIGRATION UUID: Completée. UserID et TrackID en UUID. func (s *TrackVersionService) RestoreVersion(ctx context.Context, trackID uuid.UUID, versionID uuid.UUID, userID uuid.UUID) error { // Vérifier que le track existe et appartient à l'utilisateur var track models.Track if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrTrackNotFound } return fmt.Errorf("failed to get track: %w", err) } if track.UserID != userID { return ErrForbidden } // Récupérer la version version, err := s.GetVersion(ctx, trackID, versionID) if err != nil { return err } // Vérifier que le fichier de la version existe if _, err := os.Stat(version.FilePath); os.IsNotExist(err) { return fmt.Errorf("version file not found: %s", version.FilePath) } // Sauvegarder l'ancien fichier du track comme backup (optionnel, on pourrait créer une version automatique) // Pour l'instant, on remplace directement // Copier le fichier de la version vers le track if err := copyFile(version.FilePath, track.FilePath); err != nil { return fmt.Errorf("failed to restore version file: %w", err) } // Mettre à jour les métadonnées du track avec les informations de la version updates := map[string]interface{}{ "file_size": version.FileSize, } if err := s.db.WithContext(ctx).Model(&track).Updates(updates).Error; err != nil { return fmt.Errorf("failed to update track: %w", err) } s.logger.Info("Track version restored", zap.String("track_id", trackID.String()), zap.String("version_id", versionID.String()), zap.Int("version_number", version.VersionNumber), zap.String("user_id", userID.String()), ) return nil } // DeleteVersion supprime une version spécifique // MIGRATION UUID: Completée. UserID et TrackID en UUID. func (s *TrackVersionService) DeleteVersion(ctx context.Context, trackID uuid.UUID, versionID uuid.UUID, userID uuid.UUID) error { // Vérifier que le track existe et appartient à l'utilisateur var track models.Track if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrTrackNotFound } return fmt.Errorf("failed to get track: %w", err) } if track.UserID != userID { return ErrForbidden } // Récupérer la version version, err := s.GetVersion(ctx, trackID, versionID) if err != nil { return err } // Supprimer le fichier de la version si il existe if version.FilePath != "" { if err := os.Remove(version.FilePath); err != nil && !os.IsNotExist(err) { s.logger.Warn("Failed to delete version file", zap.String("version_id", versionID.String()), zap.String("file_path", version.FilePath), zap.Error(err), ) // On continue même si la suppression du fichier échoue } } // Supprimer la version de la base de données (soft delete) if err := s.db.WithContext(ctx).Delete(version).Error; err != nil { return fmt.Errorf("failed to delete version: %w", err) } s.logger.Info("Track version deleted", zap.String("track_id", trackID.String()), zap.String("version_id", versionID.String()), zap.String("user_id", userID.String()), ) return nil } // copyFile est une fonction utilitaire pour copier un fichier func copyFile(src, dst string) error { // Créer le répertoire de destination si nécessaire dstDir := filepath.Dir(dst) if err := os.MkdirAll(dstDir, 0755); err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } sourceFile, err := os.Open(src) if err != nil { return fmt.Errorf("failed to open source file: %w", err) } defer sourceFile.Close() destinationFile, err := os.Create(dst) if err != nil { return fmt.Errorf("failed to create destination file: %w", err) } defer destinationFile.Close() _, err = io.Copy(destinationFile, sourceFile) if err != nil { return fmt.Errorf("failed to copy file: %w", err) } return nil }