This commit is contained in:
@@ -19,6 +19,24 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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 v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
|||||||
@@ -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 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"gestion/db"
|
"gestion/db"
|
||||||
"gestion/models"
|
"gestion/models"
|
||||||
|
"gestion/services"
|
||||||
"gestion/utils"
|
"gestion/utils"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -733,6 +735,7 @@ func UpdateStock(c *gin.Context) {
|
|||||||
|
|
||||||
func DeleteMedia(c *gin.Context) {
|
func DeleteMedia(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
s3Service := c.MustGet("s3Service").(*services.S3Service)
|
||||||
|
|
||||||
// ✅ VÉRIFIER LE RÔLE
|
// ✅ VÉRIFIER LE RÔLE
|
||||||
role := c.GetString("role")
|
role := c.GetString("role")
|
||||||
@@ -753,28 +756,24 @@ func DeleteMedia(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ SÉCURISER LE CHEMIN AVANT SUPPRESSION
|
// ✅ SUPPRIMER DE LA DB EN PREMIER (source de vérité)
|
||||||
filePath := strings.TrimPrefix(media.URL, "/")
|
if err := database.DeleteMedia(mediaID); err != nil {
|
||||||
|
log.Printf("❌ [DeleteMedia] Erreur suppression DB: %v", err)
|
||||||
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 {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression"})
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Média supprimé",
|
"message": "Média supprimé",
|
||||||
@@ -783,6 +782,7 @@ func DeleteMedia(c *gin.Context) {
|
|||||||
|
|
||||||
func UploadMedia(c *gin.Context) {
|
func UploadMedia(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
s3Service := c.MustGet("s3Service").(*services.S3Service)
|
||||||
|
|
||||||
// ✅ VÉRIFIER LE RÔLE
|
// ✅ VÉRIFIER LE RÔLE
|
||||||
username, err := safeGetUsername(c)
|
username, err := safeGetUsername(c)
|
||||||
@@ -792,7 +792,7 @@ func UploadMedia(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
role := c.GetString("role")
|
role := c.GetString("role")
|
||||||
if role != "admin" && role != "cabine" {
|
if role != "admin" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -860,36 +860,33 @@ func UploadMedia(c *gin.Context) {
|
|||||||
cleanProductName := cleanFileName(productName)
|
cleanProductName := cleanFileName(productName)
|
||||||
uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, file.Filename)
|
uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, file.Filename)
|
||||||
|
|
||||||
// ✅ CRÉER LE DOSSIER
|
// ✅ UPLOAD VERS RUSTFS (remplace la sauvegarde disque locale)
|
||||||
destFolder := filepath.Join("uploads", fileType+"s")
|
folder := fileType + "s" // "images" ou "videos"
|
||||||
if err := os.MkdirAll(destFolder, 0750); err != nil {
|
key, err := s3Service.UploadFileWithName(file, folder, uniqueFileName)
|
||||||
log.Printf("❌ [UploadMedia] Erreur création dossier: %v", err)
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création dossier"})
|
log.Printf("❌ [UploadMedia] Erreur upload RustFS: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur upload fichier"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ SAUVEGARDER LE FICHIER
|
log.Printf("✅ [UploadMedia] Fichier uploadé sur RustFS: %s", key)
|
||||||
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)
|
|
||||||
|
|
||||||
// ✅ CRÉER L'ENTRÉE EN BASE
|
// ✅ 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{
|
media := models.Media{
|
||||||
ProductID: productID,
|
ProductID: productID,
|
||||||
Type: fileType,
|
Type: fileType,
|
||||||
URL: mediaURL,
|
URL: "/media/" + key,
|
||||||
|
Key: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = database.CreateMedia(&media)
|
err = database.CreateMedia(&media)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Rollback: supprimer le fichier
|
// ✅ Rollback: supprimer le fichier sur RustFS
|
||||||
os.Remove(filePath)
|
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)
|
log.Printf("❌ [UploadMedia] Erreur DB: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"})
|
||||||
return
|
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) {
|
func ActivePrice(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
role := c.GetString("role")
|
role := c.GetString("role")
|
||||||
|
|||||||
+16
-2
@@ -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("")
|
||||||
log.Println("🧹 Démarrage du nettoyage des commandes invalides...")
|
log.Println("🧹 Démarrage du nettoyage des commandes invalides...")
|
||||||
removed, err := database.CleanupInvalidQueueCommands()
|
removed, err := database.CleanupInvalidQueueCommands()
|
||||||
@@ -153,6 +166,7 @@ func main() {
|
|||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("database", database)
|
c.Set("database", database)
|
||||||
c.Set("geoService", geoService)
|
c.Set("geoService", geoService)
|
||||||
|
c.Set("s3Service", s3Service)
|
||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -165,9 +179,9 @@ func main() {
|
|||||||
c.Next()
|
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 {
|
if err := r.Run(":8080"); err != nil {
|
||||||
log.Fatalf("❌ Erreur au lancement du serveur : %v", err)
|
log.Fatalf("❌ Erreur au lancement du serveur : %v", err)
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package models
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Media struct {
|
type Media struct {
|
||||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID int `json:"id"`
|
||||||
ProductID int `gorm:"column:product_id" json:"product_id"`
|
ProductID int `json:"product_id"`
|
||||||
Type string `gorm:"column:type" json:"type"`
|
Type string `json:"type"`
|
||||||
URL string `gorm:"column:url" json:"url"`
|
URL string `json:"url"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Key string `json:"-"` // clé interne RustFS, jamais exposée
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Media) TableName() string { return "media" }
|
func (Media) TableName() string { return "media" }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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
|
// 🔐 MIDDLEWARE GLOBAL
|
||||||
@@ -17,6 +17,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
router.Use(func(c *gin.Context) {
|
router.Use(func(c *gin.Context) {
|
||||||
c.Set("database", database)
|
c.Set("database", database)
|
||||||
c.Set("geoService", geoService)
|
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)
|
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)
|
// 🤖 WEBHOOK TELEGRAM - PUBLIC (sécurisé par secret header)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user