chore: build
Backend - Build & Lint / build (push) Failing after 30m33s

This commit is contained in:
Nuxgrid
2026-08-01 21:51:32 +02:00
parent 0d9b4adc3a
commit 020ba7c1e4
6 changed files with 187 additions and 69 deletions
+40 -69
View File
@@ -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)
}
}
+11
View File
@@ -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()
})
+105
View File
@@ -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)
}
+17
View File
@@ -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
}