veza/veza-backend-api/internal/services/track_version_service.go
2026-03-05 23:03:43 +01:00

271 lines
8 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/google/uuid"
"veza-backend-api/internal/models"
"go.uber.org/zap"
"gorm.io/gorm"
)
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
}