package services import ( "fmt" "gestion/utils" "io" "mime/multipart" "os" "path/filepath" ) // Storage abstrait l'emplacement de stockage des médias produits (local ou S3), // pour que tous les points d'upload/suppression respectent le même driver. type Storage interface { // Upload sauvegarde le fichier et renvoie l'URL à persister en DB (models.Media.URL) // et la clé interne (vide pour local, clé S3 sinon — models.Media.Key). Upload(fileHeader *multipart.FileHeader, folder, fileName string) (url string, key string, err error) // Delete supprime le fichier. url et key sont ceux stockés en DB pour ce média : // chaque implémentation ignore celui qui ne la concerne pas. Delete(url string, key string) error } // LocalStorage stocke les fichiers sur le disque local, sous baseDir (ex: "uploads"). type LocalStorage struct { baseDir string } func NewLocalStorage(baseDir string) *LocalStorage { return &LocalStorage{baseDir: baseDir} } func (s *LocalStorage) Upload(fileHeader *multipart.FileHeader, folder, fileName string) (url string, key string, err error) { destFolder := filepath.Join(s.baseDir, folder) if err := os.MkdirAll(destFolder, 0750); err != nil { return "", "", fmt.Errorf("erreur création dossier: %w", err) } filePath := filepath.Join(destFolder, fileName) safeFilePath, err := utils.SanitizeFilePath(filePath) if err != nil { return "", "", fmt.Errorf("chemin invalide: %w", err) } src, err := fileHeader.Open() if err != nil { return "", "", fmt.Errorf("erreur ouverture fichier: %w", err) } defer src.Close() dst, err := os.OpenFile(safeFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640) if err != nil { return "", "", fmt.Errorf("erreur création fichier: %w", err) } defer dst.Close() if _, err := io.Copy(dst, src); err != nil { os.Remove(safeFilePath) return "", "", fmt.Errorf("erreur écriture fichier: %w", err) } return "/" + filepath.ToSlash(safeFilePath), "", nil } func (s *LocalStorage) Delete(url string, key string) error { filePath := "" if len(url) > 0 && url[0] == '/' { filePath = url[1:] } else { filePath = url } safeFilePath, err := utils.SanitizeFilePath(filePath) if err != nil { return fmt.Errorf("chemin invalide: %w", err) } if err := os.Remove(safeFilePath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("erreur suppression: %w", err) } return nil } // S3Storage adapte le S3Service existant (RustFS) à l'interface Storage. type S3Storage struct { s3 *S3Service } func NewS3Storage(s3 *S3Service) *S3Storage { return &S3Storage{s3: s3} } func (s *S3Storage) Upload(fileHeader *multipart.FileHeader, folder, fileName string) (url string, key string, err error) { key, err = s.s3.UploadFileWithName(fileHeader, folder, fileName) if err != nil { return "", "", err } return "/media/" + key, key, nil } func (s *S3Storage) Delete(url string, key string) error { if key == "" { return fmt.Errorf("clé S3 manquante pour suppression") } return s.s3.DeleteFile(key) }