This commit is contained in:
@@ -10,8 +10,6 @@ import (
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -142,20 +140,6 @@ func validateFileMimeType(fileHeader *multipart.FileHeader) (string, error) {
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
func sanitizeFilePath(path string) (string, error) {
|
||||
cleaned := filepath.Clean(path)
|
||||
|
||||
if strings.Contains(cleaned, "..") {
|
||||
return "", fmt.Errorf("path traversal détecté")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(cleaned, "uploads/") && !strings.HasPrefix(cleaned, "uploads\\") {
|
||||
return "", fmt.Errorf("chemin invalide")
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func CreateProduct(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -324,12 +308,13 @@ func CreateProduct(c *gin.Context) {
|
||||
|
||||
cleanProductName := cleanFileName(product.Name)
|
||||
uploadedMedia := []models.Media{}
|
||||
savedFiles := []string{}
|
||||
savedFiles := []models.Media{}
|
||||
storage := c.MustGet("storage").(services.Storage)
|
||||
var totalSize int64 = 0
|
||||
|
||||
for i, fileHeader := range files {
|
||||
if fileHeader.Size > MaxFileSize {
|
||||
rollbackFiles(savedFiles)
|
||||
rollbackFiles(storage, savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Fichier %s trop volumineux (max %dMB)", fileHeader.Filename, MaxFileSize/(1024*1024)),
|
||||
@@ -339,7 +324,7 @@ func CreateProduct(c *gin.Context) {
|
||||
|
||||
totalSize += fileHeader.Size
|
||||
if totalSize > MaxTotalUploadSize {
|
||||
rollbackFiles(savedFiles)
|
||||
rollbackFiles(storage, savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Taille totale dépassée (max %dMB)", MaxTotalUploadSize/(1024*1024)),
|
||||
@@ -352,7 +337,7 @@ func CreateProduct(c *gin.Context) {
|
||||
mimeType, err := validateFileMimeType(fileHeader)
|
||||
if err != nil {
|
||||
log.Printf("❌ [CreateProduct] Type MIME invalide: %v", err)
|
||||
rollbackFiles(savedFiles)
|
||||
rollbackFiles(storage, savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Type de fichier non autorisé"})
|
||||
return
|
||||
@@ -364,7 +349,7 @@ func CreateProduct(c *gin.Context) {
|
||||
} else if strings.HasPrefix(mimeType, "video/") {
|
||||
mediaType = "video"
|
||||
} else {
|
||||
rollbackFiles(savedFiles)
|
||||
rollbackFiles(storage, savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Type de média non supporté"})
|
||||
return
|
||||
@@ -372,46 +357,27 @@ func CreateProduct(c *gin.Context) {
|
||||
|
||||
uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, fileHeader.Filename)
|
||||
|
||||
destFolder := filepath.Join("uploads", mediaType+"s")
|
||||
if err := os.MkdirAll(destFolder, 0750); err != nil {
|
||||
log.Printf("❌ [CreateProduct] Erreur création dossier: %v", err)
|
||||
rollbackFiles(savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur système"})
|
||||
return
|
||||
}
|
||||
|
||||
filePath := filepath.Join(destFolder, uniqueFileName)
|
||||
|
||||
safeFilePath, err := sanitizeFilePath(filePath)
|
||||
mediaURL, mediaKey, err := storage.Upload(fileHeader, mediaType+"s", uniqueFileName)
|
||||
if err != nil {
|
||||
log.Printf("❌ [CreateProduct] Path traversal détecté: %v", err)
|
||||
rollbackFiles(savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Chemin invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.SaveUploadedFile(fileHeader, safeFilePath); err != nil {
|
||||
log.Printf("❌ [CreateProduct] Erreur sauvegarde: %v", err)
|
||||
rollbackFiles(savedFiles)
|
||||
rollbackFiles(storage, savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur sauvegarde fichier"})
|
||||
return
|
||||
}
|
||||
|
||||
savedFiles = append(savedFiles, safeFilePath)
|
||||
savedFiles = append(savedFiles, models.Media{URL: mediaURL, Key: mediaKey})
|
||||
|
||||
mediaURL := "/" + filepath.ToSlash(safeFilePath)
|
||||
media := models.Media{
|
||||
ProductID: product.ID,
|
||||
Type: mediaType,
|
||||
URL: mediaURL,
|
||||
Key: mediaKey,
|
||||
}
|
||||
|
||||
if err := database.CreateMedia(&media); err != nil {
|
||||
log.Printf("❌ [CreateProduct] Erreur DB média: %v", err)
|
||||
rollbackFiles(savedFiles)
|
||||
rollbackFiles(storage, savedFiles)
|
||||
database.DeleteProduct(product.ID)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"})
|
||||
return
|
||||
@@ -736,6 +702,13 @@ func DeleteMedia(c *gin.Context) {
|
||||
} else {
|
||||
log.Printf("✅ [DeleteMedia] Fichier supprimé sur RustFS: %s", media.Key)
|
||||
}
|
||||
} else {
|
||||
localStorage := services.NewLocalStorage("uploads")
|
||||
if err := localStorage.Delete(media.URL, ""); err != nil {
|
||||
log.Printf("⚠️ [DeleteMedia] Fichier local non supprimé (%s): %v", media.URL, err)
|
||||
} else {
|
||||
log.Printf("✅ [DeleteMedia] Fichier local supprimé: %s", media.URL)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -746,7 +719,6 @@ func DeleteMedia(c *gin.Context) {
|
||||
|
||||
func UploadMedia(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
s3Service := c.MustGet("s3Service").(*services.S3Service)
|
||||
|
||||
username, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
@@ -816,27 +788,28 @@ func UploadMedia(c *gin.Context) {
|
||||
cleanProductName := cleanFileName(productName)
|
||||
uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, file.Filename)
|
||||
|
||||
storage := c.MustGet("storage").(services.Storage)
|
||||
folder := fileType + "s"
|
||||
key, err := s3Service.UploadFileWithName(file, folder, uniqueFileName)
|
||||
mediaURL, mediaKey, err := storage.Upload(file, folder, uniqueFileName)
|
||||
if err != nil {
|
||||
log.Printf("❌ [UploadMedia] Erreur upload RustFS: %v", err)
|
||||
log.Printf("❌ [UploadMedia] Erreur upload: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur upload fichier"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [UploadMedia] Fichier uploadé sur RustFS: %s", key)
|
||||
log.Printf("✅ [UploadMedia] Fichier uploadé: %s", mediaURL)
|
||||
|
||||
media := models.Media{
|
||||
ProductID: productID,
|
||||
Type: fileType,
|
||||
URL: "/media/" + key,
|
||||
Key: key,
|
||||
URL: mediaURL,
|
||||
Key: mediaKey,
|
||||
}
|
||||
|
||||
err = database.CreateMedia(&media)
|
||||
if err != nil {
|
||||
if delErr := s3Service.DeleteFile(key); delErr != nil {
|
||||
log.Printf("⚠️ [UploadMedia] Échec rollback RustFS (clé: %s): %v", key, delErr)
|
||||
if delErr := storage.Delete(mediaURL, mediaKey); delErr != nil {
|
||||
log.Printf("⚠️ [UploadMedia] Échec rollback (%s): %v", mediaURL, delErr)
|
||||
}
|
||||
log.Printf("❌ [UploadMedia] Erreur DB: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"})
|
||||
@@ -948,17 +921,17 @@ func DeleteProduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
s3Service := c.MustGet("s3Service").(*services.S3Service)
|
||||
localStorage := services.NewLocalStorage("uploads")
|
||||
for _, media := range mediaList {
|
||||
filePath := strings.TrimPrefix(media.URL, "/")
|
||||
|
||||
safeFilePath, err := sanitizeFilePath(filePath)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [DeleteProduct] Path invalide: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Remove(safeFilePath); err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("⚠️ [DeleteProduct] Erreur suppression: %v", err)
|
||||
if media.Key != "" {
|
||||
if err := s3Service.DeleteFile(media.Key); err != nil {
|
||||
log.Printf("⚠️ [DeleteProduct] Fichier non supprimé sur RustFS (clé: %s): %v", media.Key, err)
|
||||
}
|
||||
} else {
|
||||
if err := localStorage.Delete(media.URL, ""); err != nil {
|
||||
log.Printf("⚠️ [DeleteProduct] Erreur suppression locale: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,13 +951,11 @@ func DeleteProduct(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func rollbackFiles(files []string) {
|
||||
for _, file := range files {
|
||||
safeFilePath, err := sanitizeFilePath(file)
|
||||
if err != nil {
|
||||
continue
|
||||
func rollbackFiles(storage services.Storage, files []models.Media) {
|
||||
for _, f := range files {
|
||||
if err := storage.Delete(f.URL, f.Key); err != nil {
|
||||
log.Printf("⚠️ [rollbackFiles] Erreur suppression %s: %v", f.URL, err)
|
||||
}
|
||||
os.Remove(safeFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,16 @@ func main() {
|
||||
log.Fatalf("erreur init S3: %v", err)
|
||||
}
|
||||
|
||||
var storage services.Storage
|
||||
switch os.Getenv("STORAGE_DRIVER") {
|
||||
case "s3":
|
||||
storage = services.NewS3Storage(s3Service)
|
||||
log.Println("✅ Storage driver: s3 (RustFS)")
|
||||
default:
|
||||
storage = services.NewLocalStorage("uploads")
|
||||
log.Println("✅ Storage driver: local")
|
||||
}
|
||||
|
||||
log.Println("")
|
||||
log.Println("🧹 Démarrage du nettoyage des commandes invalides...")
|
||||
removed, err := database.CleanupInvalidQueueCommands()
|
||||
@@ -167,6 +177,7 @@ func main() {
|
||||
c.Set("database", database)
|
||||
c.Set("geoService", geoService)
|
||||
c.Set("s3Service", s3Service)
|
||||
c.Set("storage", storage)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GenerateUniqueFileName(productName string, originalFileName string) string {
|
||||
@@ -13,3 +14,19 @@ func GenerateUniqueFileName(productName string, originalFileName string) string
|
||||
ext := filepath.Ext(originalFileName)
|
||||
return fmt.Sprintf("%s_%s%s", productName, randomString, ext)
|
||||
}
|
||||
|
||||
// SanitizeFilePath valide qu'un chemin de fichier local reste bien dans le
|
||||
// dossier uploads/ et ne contient pas de tentative de path traversal.
|
||||
func SanitizeFilePath(path string) (string, error) {
|
||||
cleaned := filepath.Clean(path)
|
||||
|
||||
if strings.Contains(cleaned, "..") {
|
||||
return "", fmt.Errorf("path traversal détecté")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(cleaned, "uploads/") && !strings.HasPrefix(cleaned, "uploads\\") {
|
||||
return "", fmt.Errorf("chemin invalide")
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user