From 1721f550605fe576fc880d7d1ed2b95c3fe87100 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sun, 28 Jun 2026 16:01:23 +0200 Subject: [PATCH] chore: build --- backend/gestion/go.mod | 18 ++++ backend/gestion/go.sum | 36 ++++++++ backend/gestion/handlers/product.go | 93 +++++++++++-------- backend/gestion/main.go | 18 +++- backend/gestion/models/media.go | 11 ++- backend/gestion/routes/routes.go | 8 +- backend/gestion/services/s3.go | 137 ++++++++++++++++++++++++++++ 7 files changed, 276 insertions(+), 45 deletions(-) create mode 100644 backend/gestion/services/s3.go diff --git a/backend/gestion/go.mod b/backend/gestion/go.mod index 79eef9b8..d0006bff 100644 --- a/backend/gestion/go.mod +++ b/backend/gestion/go.mod @@ -19,6 +19,24 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.25 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.1 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/backend/gestion/go.sum b/backend/gestion/go.sum index 5c211e60..e25cce69 100644 --- a/backend/gestion/go.sum +++ b/backend/gestion/go.sum @@ -1,3 +1,39 @@ +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 h1:ta8csKy5vN91F3i5gGR85lFV0srBqySEji7Jroes6rE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= diff --git a/backend/gestion/handlers/product.go b/backend/gestion/handlers/product.go index ab291227..5059c355 100644 --- a/backend/gestion/handlers/product.go +++ b/backend/gestion/handlers/product.go @@ -4,7 +4,9 @@ import ( "fmt" "gestion/db" "gestion/models" + "gestion/services" "gestion/utils" + "io" "log" "mime/multipart" "net/http" @@ -733,6 +735,7 @@ func UpdateStock(c *gin.Context) { func DeleteMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) + s3Service := c.MustGet("s3Service").(*services.S3Service) // ✅ VÉRIFIER LE RÔLE role := c.GetString("role") @@ -753,28 +756,24 @@ func DeleteMedia(c *gin.Context) { return } - // ✅ SÉCURISER LE CHEMIN AVANT SUPPRESSION - filePath := strings.TrimPrefix(media.URL, "/") - - safeFilePath, err := sanitizeFilePath(filePath) - if err != nil { - log.Printf("❌ [DeleteMedia] Path invalide: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Chemin invalide"}) - return - } - - // ✅ SUPPRIMER LE FICHIER PHYSIQUE - if err := os.Remove(safeFilePath); err != nil && !os.IsNotExist(err) { - log.Printf("⚠️ [DeleteMedia] Erreur suppression fichier: %v", err) - } - - // ✅ SUPPRIMER DE LA DB - err = database.DeleteMedia(mediaID) - if err != nil { + // ✅ SUPPRIMER DE LA DB EN PREMIER (source de vérité) + if err := database.DeleteMedia(mediaID); err != nil { + log.Printf("❌ [DeleteMedia] Erreur suppression DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression"}) return } + // ✅ SUPPRIMER LE FICHIER SUR RUSTFS + if media.Key != "" { + if err := s3Service.DeleteFile(media.Key); err != nil { + // On ne fait pas échouer la requête : l'entrée DB est déjà supprimée, + // mais on log pour pouvoir nettoyer manuellement un fichier orphelin si besoin. + log.Printf("⚠️ [DeleteMedia] Fichier non supprimé sur RustFS (clé: %s): %v", media.Key, err) + } else { + log.Printf("✅ [DeleteMedia] Fichier supprimé sur RustFS: %s", media.Key) + } + } + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Média supprimé", @@ -783,6 +782,7 @@ func DeleteMedia(c *gin.Context) { func UploadMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) + s3Service := c.MustGet("s3Service").(*services.S3Service) // ✅ VÉRIFIER LE RÔLE username, err := safeGetUsername(c) @@ -792,7 +792,7 @@ func UploadMedia(c *gin.Context) { } role := c.GetString("role") - if role != "admin" && role != "cabine" { + if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } @@ -860,36 +860,33 @@ func UploadMedia(c *gin.Context) { cleanProductName := cleanFileName(productName) uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, file.Filename) - // ✅ CRÉER LE DOSSIER - destFolder := filepath.Join("uploads", fileType+"s") - if err := os.MkdirAll(destFolder, 0750); err != nil { - log.Printf("❌ [UploadMedia] Erreur création dossier: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création dossier"}) + // ✅ UPLOAD VERS RUSTFS (remplace la sauvegarde disque locale) + folder := fileType + "s" // "images" ou "videos" + key, err := s3Service.UploadFileWithName(file, folder, uniqueFileName) + if err != nil { + log.Printf("❌ [UploadMedia] Erreur upload RustFS: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur upload fichier"}) return } - // ✅ SAUVEGARDER LE FICHIER - filePath := filepath.Join(destFolder, uniqueFileName) - if err := c.SaveUploadedFile(file, filePath); err != nil { - log.Printf("❌ [UploadMedia] Erreur sauvegarde: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur sauvegarde fichier"}) - return - } - - log.Printf("✅ [UploadMedia] Fichier sauvegardé: %s", filePath) + log.Printf("✅ [UploadMedia] Fichier uploadé sur RustFS: %s", key) // ✅ CRÉER L'ENTRÉE EN BASE - mediaURL := "/" + filepath.ToSlash(filePath) + // L'URL exposée passe par notre proxy /media/:key (RustFS est derrière le VPN, + // donc inaccessible directement depuis le client/navigateur). media := models.Media{ ProductID: productID, Type: fileType, - URL: mediaURL, + URL: "/media/" + key, + Key: key, } err = database.CreateMedia(&media) if err != nil { - // Rollback: supprimer le fichier - os.Remove(filePath) + // ✅ Rollback: supprimer le fichier sur RustFS + if delErr := s3Service.DeleteFile(key); delErr != nil { + log.Printf("⚠️ [UploadMedia] Échec rollback RustFS (clé: %s): %v", key, delErr) + } log.Printf("❌ [UploadMedia] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"}) return @@ -908,6 +905,28 @@ func UploadMedia(c *gin.Context) { }) } +func ServeMedia(c *gin.Context) { + s3Service := c.MustGet("s3Service").(*services.S3Service) + + key := strings.TrimPrefix(c.Param("key"), "/") + if key == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Clé manquante"}) + return + } + + body, contentType, err := s3Service.GetFile(key) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Média non trouvé"}) + return + } + defer body.Close() + + c.Header("Content-Type", contentType) + c.Header("Cache-Control", "public, max-age=31536000, immutable") + c.Status(http.StatusOK) + io.Copy(c.Writer, body) +} + func ActivePrice(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") diff --git a/backend/gestion/main.go b/backend/gestion/main.go index 1c5c65b5..4e010c73 100644 --- a/backend/gestion/main.go +++ b/backend/gestion/main.go @@ -92,6 +92,19 @@ func main() { }() } + s3Service, err := services.NewS3Service( + os.Getenv("S3_REGION"), + os.Getenv("S3_BUCKET"), + os.Getenv("S3_ENDPOINT"), + services.S3Credentials{ + S3KeyId: os.Getenv("RUSTFS_ACCESS_KEY"), + S3AccessKey: os.Getenv("RUSTFS_SECRET_KEY"), + }, + ) + if err != nil { + log.Fatalf("erreur init S3: %v", err) + } + log.Println("") log.Println("🧹 Démarrage du nettoyage des commandes invalides...") removed, err := database.CleanupInvalidQueueCommands() @@ -153,6 +166,7 @@ func main() { r.Use(func(c *gin.Context) { c.Set("database", database) c.Set("geoService", geoService) + c.Set("s3Service", s3Service) c.Next() }) @@ -165,9 +179,9 @@ func main() { c.Next() }) - r.Static("/uploads", "./uploads") + //r.Static("/uploads", "./uploads") - routes.SetupRoutes(r, database, geoService) + routes.SetupRoutes(r, database, geoService, s3Service) if err := r.Run(":8080"); err != nil { log.Fatalf("❌ Erreur au lancement du serveur : %v", err) diff --git a/backend/gestion/models/media.go b/backend/gestion/models/media.go index 9dfc41dd..10b715b9 100644 --- a/backend/gestion/models/media.go +++ b/backend/gestion/models/media.go @@ -3,11 +3,12 @@ package models import "time" type Media struct { - ID int `gorm:"primaryKey;autoIncrement" json:"id"` - ProductID int `gorm:"column:product_id" json:"product_id"` - Type string `gorm:"column:type" json:"type"` - URL string `gorm:"column:url" json:"url"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + ID int `json:"id"` + ProductID int `json:"product_id"` + Type string `json:"type"` + URL string `json:"url"` + Key string `json:"-"` // clé interne RustFS, jamais exposée + CreatedAt time.Time `json:"created_at"` } func (Media) TableName() string { return "media" } diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index 447556a1..d2c6092c 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" ) -func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services.GeoService) { +func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services.GeoService, s3Service *services.S3Service) { // ============================================ // 🔐 MIDDLEWARE GLOBAL @@ -17,6 +17,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services router.Use(func(c *gin.Context) { c.Set("database", database) c.Set("geoService", geoService) + c.Set("s3Service", s3Service) }) // ============================================ @@ -129,6 +130,11 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // ============================================ router.POST("/api/v1/webhooks/nowpayments", handlers.IPNWebhook) + // ============================================ + // 🖼️ PROXY MÉDIAS (RustFS privé via VPN) + // ============================================ + router.GET("/media/*key", handlers.ServeMedia) + // ============================================ // 🤖 WEBHOOK TELEGRAM - PUBLIC (sécurisé par secret header) // ============================================ diff --git a/backend/gestion/services/s3.go b/backend/gestion/services/s3.go new file mode 100644 index 00000000..bcf31e3c --- /dev/null +++ b/backend/gestion/services/s3.go @@ -0,0 +1,137 @@ +package services + +import ( + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "path/filepath" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" +) + +type S3Service struct { + client *s3.Client + bucketName string +} + +type S3Credentials struct { + S3KeyId string + S3AccessKey string +} + +// NewS3Service initialise le client S3 pointant vers RustFS (accessible via VPN). +func NewS3Service(region, bucketName, endpoint string, creds S3Credentials) (*S3Service, error) { + var cfg aws.Config + var err error + + if creds.S3KeyId != "" && creds.S3AccessKey != "" { + cfg, err = config.LoadDefaultConfig(context.TODO(), + config.WithRegion(region), + config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider(creds.S3KeyId, creds.S3AccessKey, ""), + ), + ) + } else { + cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + } + if err != nil { + return nil, fmt.Errorf("erreur chargement config AWS: %w", err) + } + + client := s3.NewFromConfig(cfg, func(o *s3.Options) { + if endpoint != "" { + o.BaseEndpoint = aws.String(endpoint) // ex: http://10.x.x.x:9000 (IP interne VPN de RustFS) + o.UsePathStyle = true + } + }) + + return &S3Service{ + client: client, + bucketName: bucketName, + }, nil +} + +// UploadFile upload un fichier et renvoie sa clé S3 (pas d'URL publique, RustFS est privé). +func (s *S3Service) UploadFile(fileHeader *multipart.FileHeader, folder string) (key string, err error) { + ext := filepath.Ext(fileHeader.Filename) + fileName := fmt.Sprintf("%s%s", uuid.New().String(), ext) + return s.UploadFileWithName(fileHeader, folder, fileName) +} + +// UploadFileWithName upload un fichier avec un nom déjà déterminé et renvoie la clé S3. +func (s *S3Service) UploadFileWithName(fileHeader *multipart.FileHeader, folder, fileName string) (key string, err error) { + file, err := fileHeader.Open() + if err != nil { + return "", fmt.Errorf("erreur ouverture fichier: %w", err) + } + defer file.Close() + + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(file); err != nil { + return "", fmt.Errorf("erreur lecture fichier: %w", err) + } + + key = fmt.Sprintf("%s/%s", folder, fileName) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + contentType := fileHeader.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/octet-stream" + } + + _, err = s.client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(key), + Body: bytes.NewReader(buf.Bytes()), + ContentType: aws.String(contentType), + }) + if err != nil { + return "", fmt.Errorf("erreur upload RustFS: %w", err) + } + + return key, nil +} + +// GetFile récupère un objet depuis RustFS (stream + content-type) pour le proxy. +func (s *S3Service) GetFile(key string) (io.ReadCloser, string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + out, err := s.client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(key), + }) + if err != nil { + return nil, "", fmt.Errorf("erreur lecture RustFS: %w", err) + } + + contentType := "application/octet-stream" + if out.ContentType != nil { + contentType = *out.ContentType + } + return out.Body, contentType, nil +} + +// DeleteFile supprime un fichier à partir de sa clé S3. +func (s *S3Service) DeleteFile(key string) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(key), + }) + if err != nil { + return fmt.Errorf("erreur suppression RustFS: %w", err) + } + return nil +}