271 lines
7.6 KiB
Go
271 lines
7.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// TrackStorageService gère le stockage des fichiers audio
|
|
type TrackStorageService struct {
|
|
localPath string
|
|
useS3 bool
|
|
s3Service interface{} // S3Service sera implémenté plus tard (T0224)
|
|
logger *zap.Logger
|
|
maxRetries int
|
|
retryDelay time.Duration
|
|
}
|
|
|
|
// S3Service interface pour le service S3 (à implémenter plus tard)
|
|
type S3Service interface {
|
|
UploadFile(ctx context.Context, data []byte, key string, contentType string) (string, error)
|
|
DeleteFile(ctx context.Context, key string) error
|
|
GetPresignedURL(ctx context.Context, key string) (string, error)
|
|
}
|
|
|
|
// NewTrackStorageService crée un nouveau service de stockage de tracks
|
|
func NewTrackStorageService(localPath string, useS3 bool, logger *zap.Logger) *TrackStorageService {
|
|
if localPath == "" {
|
|
localPath = "uploads/tracks"
|
|
}
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
return &TrackStorageService{
|
|
localPath: localPath,
|
|
useS3: useS3,
|
|
logger: logger,
|
|
maxRetries: 3,
|
|
retryDelay: time.Second * 2,
|
|
}
|
|
}
|
|
|
|
// SetS3Service définit le service S3 (quand il sera disponible)
|
|
func (s *TrackStorageService) SetS3Service(s3Service S3Service) {
|
|
s.s3Service = s3Service
|
|
s.useS3 = s3Service != nil
|
|
}
|
|
|
|
// GetDownloadURL retourne une URL de téléchargement (signée pour S3, relative pour local)
|
|
func (s *TrackStorageService) GetDownloadURL(ctx context.Context, filePath string) (string, error) {
|
|
if s.useS3 && s.s3Service != nil {
|
|
s3Service, ok := s.s3Service.(S3Service)
|
|
if !ok {
|
|
return "", fmt.Errorf("invalid S3 service type")
|
|
}
|
|
// On suppose que filePath contient la clé ou l'URL complète.
|
|
// Pour simplifier, on considère que filePath est la clé si on utilise S3.
|
|
// En réalité, il faudrait extraire la clé de l'URL stockée si nécessaire.
|
|
return s3Service.GetPresignedURL(ctx, filePath)
|
|
}
|
|
|
|
// Local storage: retourner le chemin tel quel (relatif)
|
|
return filePath, nil
|
|
}
|
|
|
|
// SaveTrack sauvegarde un fichier audio avec structure tracks/{user_id}/{track_id}/{filename}
|
|
// MIGRATION UUID: userID migré vers uuid.UUID, trackID reste int64
|
|
func (s *TrackStorageService) SaveTrack(ctx context.Context, userID uuid.UUID, trackID int64, fileHeader *multipart.FileHeader) (string, error) {
|
|
// Générer nom fichier unique
|
|
ext := filepath.Ext(fileHeader.Filename)
|
|
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
|
|
|
|
// Chemin: tracks/{user_id}/{trackID}/{filename}
|
|
key := fmt.Sprintf("tracks/%s/%d/%s", userID.String(), trackID, filename)
|
|
|
|
var filePath string
|
|
var err error
|
|
|
|
// Retry logic
|
|
for attempt := 0; attempt < s.maxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
s.logger.Warn("Retrying file upload",
|
|
zap.Int("attempt", attempt+1),
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int64("track_id", trackID),
|
|
)
|
|
time.Sleep(s.retryDelay * time.Duration(attempt))
|
|
}
|
|
|
|
if s.useS3 && s.s3Service != nil {
|
|
filePath, err = s.saveToS3(ctx, fileHeader, key)
|
|
} else {
|
|
filePath, err = s.saveLocally(fileHeader, key)
|
|
}
|
|
|
|
if err == nil {
|
|
s.logger.Info("Track file saved successfully",
|
|
zap.String("path", filePath),
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int64("track_id", trackID),
|
|
)
|
|
return filePath, nil
|
|
}
|
|
|
|
s.logger.Error("Failed to save track file",
|
|
zap.Error(err),
|
|
zap.Int("attempt", attempt+1),
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int64("track_id", trackID),
|
|
)
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to save track file after %d attempts: %w", s.maxRetries, err)
|
|
}
|
|
|
|
// saveToS3 sauvegarde le fichier vers S3
|
|
func (s *TrackStorageService) saveToS3(ctx context.Context, fileHeader *multipart.FileHeader, key string) (string, error) {
|
|
if s.s3Service == nil {
|
|
return "", fmt.Errorf("S3 service not configured")
|
|
}
|
|
|
|
// Ouvrir le fichier
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Lire le fichier en bytes
|
|
fileBytes := make([]byte, fileHeader.Size)
|
|
n, err := io.ReadFull(file, fileBytes)
|
|
if err != nil && err != io.ErrUnexpectedEOF {
|
|
return "", fmt.Errorf("failed to read file: %w", err)
|
|
}
|
|
fileBytes = fileBytes[:n]
|
|
|
|
// Déterminer le Content-Type
|
|
contentType := fileHeader.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
ext := filepath.Ext(fileHeader.Filename)
|
|
contentType = s.getContentTypeFromExtension(ext)
|
|
}
|
|
|
|
// Upload vers S3
|
|
s3Service, ok := s.s3Service.(S3Service)
|
|
if !ok {
|
|
return "", fmt.Errorf("invalid S3 service type")
|
|
}
|
|
|
|
url, err := s3Service.UploadFile(ctx, fileBytes, key, contentType)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
|
}
|
|
|
|
return url, nil
|
|
}
|
|
|
|
// saveLocally sauvegarde le fichier localement
|
|
func (s *TrackStorageService) saveLocally(fileHeader *multipart.FileHeader, key string) (string, error) {
|
|
// Chemin complet local
|
|
destPath := filepath.Join(s.localPath, key)
|
|
|
|
// Créer les répertoires nécessaires
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
return "", fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
// Ouvrir le fichier source
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Créer le fichier de destination
|
|
destFile, err := os.Create(destPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create file: %w", err)
|
|
}
|
|
defer destFile.Close()
|
|
|
|
// Copier le contenu
|
|
if _, err := io.Copy(destFile, file); err != nil {
|
|
// Nettoyer en cas d'erreur
|
|
os.Remove(destPath)
|
|
return "", fmt.Errorf("failed to save file: %w", err)
|
|
}
|
|
|
|
// Retourner le chemin relatif pour l'URL
|
|
relativePath := fmt.Sprintf("/uploads/%s", key)
|
|
return relativePath, nil
|
|
}
|
|
|
|
// DeleteTrack supprime un fichier audio
|
|
func (s *TrackStorageService) DeleteTrack(ctx context.Context, userID, trackID int64, filename string) error {
|
|
key := fmt.Sprintf("tracks/%d/%d/%s", userID, trackID, filename)
|
|
|
|
if s.useS3 && s.s3Service != nil {
|
|
return s.deleteFromS3(ctx, key)
|
|
}
|
|
|
|
return s.deleteLocally(key)
|
|
}
|
|
|
|
// deleteFromS3 supprime le fichier de S3
|
|
func (s *TrackStorageService) deleteFromS3(ctx context.Context, key string) error {
|
|
if s.s3Service == nil {
|
|
return fmt.Errorf("S3 service not configured")
|
|
}
|
|
|
|
s3Service, ok := s.s3Service.(S3Service)
|
|
if !ok {
|
|
return fmt.Errorf("invalid S3 service type")
|
|
}
|
|
|
|
if err := s3Service.DeleteFile(ctx, key); err != nil {
|
|
return fmt.Errorf("failed to delete from S3: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deleteLocally supprime le fichier localement
|
|
func (s *TrackStorageService) deleteLocally(key string) error {
|
|
destPath := filepath.Join(s.localPath, key)
|
|
|
|
if err := os.Remove(destPath); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to delete file: %w", err)
|
|
}
|
|
// Le fichier n'existe pas, considérer comme succès
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getContentTypeFromExtension retourne le Content-Type basé sur l'extension
|
|
func (s *TrackStorageService) getContentTypeFromExtension(ext string) string {
|
|
ext = filepath.Ext(ext)
|
|
switch ext {
|
|
case ".mp3":
|
|
return "audio/mpeg"
|
|
case ".flac":
|
|
return "audio/flac"
|
|
case ".wav":
|
|
return "audio/wav"
|
|
case ".ogg":
|
|
return "audio/ogg"
|
|
case ".m4a", ".aac":
|
|
return "audio/m4a"
|
|
default:
|
|
return "application/octet-stream"
|
|
}
|
|
}
|
|
|
|
// GenerateTrackKey génère une clé S3 pour un track
|
|
func (s *TrackStorageService) GenerateTrackKey(userID, trackID int64, filename string) string {
|
|
ext := filepath.Ext(filename)
|
|
if ext == "" {
|
|
ext = ".mp3" // Par défaut
|
|
}
|
|
uniqueFilename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
|
|
return fmt.Sprintf("tracks/%d/%d/%s", userID, trackID, uniqueFilename)
|
|
}
|