package routes import ( "canguidev/shelfy/internal/controllers" "canguidev/shelfy/internal/middlewares" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" ) func AddRoutes(superRoute *gin.RouterGroup,db *gorm.DB) { userRoutes(superRoute,db) testAcces(superRoute) loginRoutes(superRoute,db) pathRoute(superRoute,db) folderRoute(superRoute,db) downloadRoute(superRoute,db) } func loginRoutes(superRoute *gin.RouterGroup,db *gorm.DB) { loginRouter := superRoute.Group("/login") { loginRouter.POST("/", controllers.Login(db)) } } func userRoutes( superRoute *gin.RouterGroup,db*gorm.DB){ userRoutes := superRoute.Group("/users") { userRoutes.POST("/",middlewares.CheckAuth(db),controllers.CreateUser(db)) userRoutes.GET("/", middlewares.CheckAuth(db),controllers.ReadAllUser(db)) userRoutes.GET("/:id", middlewares.CheckAuth(db),controllers.FindUserById(db)) userRoutes.PUT("/:id", middlewares.CheckAuth(db),controllers.UpdateUser(db)) userRoutes.DELETE("/:id", middlewares.CheckAuth(db),controllers.DeleteUser(db)) } } func testAcces(superRoute *gin.RouterGroup){ test := superRoute.Group("/test") { test.GET("/",controllers.TestPing()) } } func pathRoute (superRoute *gin.RouterGroup,db *gorm.DB){ pathRoutes := superRoute.Group("/path") { pathRoutes.POST("/save-path", middlewares.CheckAuth(db),controllers.CreateSavePath(db)) pathRoutes.GET("/save-path", middlewares.CheckAuth(db),controllers.ReadAllSavePath(db)) pathRoutes.PUT("/save-path/:id", middlewares.CheckAuth(db),controllers.UpdateSavePath(db)) pathRoutes.DELETE("/save-path/:id", middlewares.CheckAuth(db),controllers.DeleteSavePath(db)) pathRoutes.POST("/save-path/validate", middlewares.CheckAuth(db),controllers.PathValidationHandler) } } func folderRoute (superRoute *gin.RouterGroup,db *gorm.DB){ folderRoutes :=superRoute.Group("/folders") { folderRoutes.GET("/",middlewares.CheckAuth(db),controllers.StreamHandler(db)) folderRoutes.GET("/details",middlewares.CheckAuth(db),controllers.DetailHandler()) folderRoutes.DELETE("/", middlewares.CheckAuth(db), controllers.DeleteHandler()) folderRoutes.GET("/stats", middlewares.CheckAuth(db), controllers.FolderStatsHandler()) folderRoutes.GET("/file", middlewares.CheckAuth(db), func(c *gin.Context) { path := c.Query("path") filePath := filepath.Join("upload", filepath.Clean("/"+path)) c.Header("Cache-Control", "no-cache, no-store, must-revalidate") c.Header("Pragma", "no-cache") c.Header("Expires", "0") // Supprime le support de "If-Modified-Since" pour forcer le 200 c.Request.Header.Del("If-Modified-Since") ext := strings.ToLower(filepath.Ext(filePath)) if ext == ".mp4" || ext == ".mkv" || ext == ".webm" || ext == ".mp3" { streamFile(c, filePath) } else { c.File(filePath) } }) } } func downloadRoute (superRoute *gin.RouterGroup,db *gorm.DB){ downloadRoutes := superRoute.Group("/download") { downloadRoutes.GET("/jobs", middlewares.CheckAuth(db), controllers.HandleListJobs(db)) downloadRoutes.GET("/debrid-status",middlewares.CheckAuth(db),controllers.GetDebridStatus(db)) downloadRoutes.POST("/account", middlewares.CheckAuth(db),controllers.PostDebridLogin(db)) downloadRoutes.POST("/add", middlewares.CheckAuth(db),controllers.HandleAddJobsMultiple(db)) downloadRoutes.POST("/jobs/:id/start", middlewares.CheckAuth(db),controllers.HandleStartJob(db)) downloadRoutes.POST("/jobs/:id/pause", middlewares.CheckAuth(db),controllers.HandlePauseJob()) downloadRoutes.POST("/jobs/:id/resume", middlewares.CheckAuth(db),controllers.HandleResumeJob(db)) downloadRoutes.DELETE("/jobs/:id", middlewares.CheckAuth(db),controllers.HandleDeleteJob(db)) } } func streamFile(c *gin.Context, filePath string) { log.Printf("[STREAM] ➡️ Requête streaming sur %s", filePath) f, err := os.Open(filePath) if err != nil { log.Printf("[STREAM][ERREUR] Impossible d'ouvrir: %v", err) c.Status(http.StatusNotFound) return } defer f.Close() fileStat, err := f.Stat() if err != nil { log.Printf("[STREAM][ERREUR] Stat impossible: %v", err) c.Status(http.StatusInternalServerError) return } fileSize := fileStat.Size() log.Printf("[STREAM] Taille du fichier: %d octets", fileSize) c.Header("Accept-Ranges", "bytes") c.Header("Content-Type", mimeTypeByExt(filePath)) rangeHeader := c.GetHeader("Range") log.Printf("[STREAM] Range header reçu: %q", rangeHeader) if rangeHeader == "" { // Pas de Range, tout le fichier c.Header("Content-Length", strconv.FormatInt(fileSize, 10)) log.Printf("[STREAM] 📦 Pas de Range, envoi du fichier complet (200 OK)") http.ServeContent(c.Writer, c.Request, fileStat.Name(), fileStat.ModTime(), f) return } // Analyse du Range var start, end int64 start, end = 0, fileSize-1 if strings.HasPrefix(rangeHeader, "bytes=") { rangeParts := strings.Split(strings.TrimPrefix(rangeHeader, "bytes="), "-") if len(rangeParts) == 2 { if s, err := strconv.ParseInt(rangeParts[0], 10, 64); err == nil { start = s } if rangeParts[1] != "" { if e, err := strconv.ParseInt(rangeParts[1], 10, 64); err == nil { end = e } } } } log.Printf("[STREAM] Parsed range: start=%d, end=%d", start, end) if start > end || start < 0 || end >= fileSize { log.Printf("[STREAM][ERREUR] Range invalide: %d-%d sur %d", start, end, fileSize) c.Status(http.StatusRequestedRangeNotSatisfiable) return } c.Status(http.StatusPartialContent) c.Header("Content-Range", "bytes "+strconv.FormatInt(start, 10)+"-"+strconv.FormatInt(end, 10)+"/"+strconv.FormatInt(fileSize, 10)) c.Header("Content-Length", strconv.FormatInt(end-start+1, 10)) log.Printf("[STREAM] ▶️ Envoi bytes %d-%d sur %d [%s]", start, end, fileSize, filePath) f.Seek(start, io.SeekStart) n, err := io.CopyN(c.Writer, f, end-start+1) if err != nil { log.Printf("[STREAM][ERREUR] pendant l'envoi: %v", err) } else { log.Printf("[STREAM] ✔️ Fini, envoyé %d octets", n) } } // Helper : extension → Content-Type (très basique) func mimeTypeByExt(filePath string) string { ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".mp4": return "video/mp4" case ".mkv": return "video/x-matroska" case ".webm": return "video/webm" case ".mp3": return "audio/mpeg" case ".pdf": return "application/pdf" default: return "application/octet-stream" } }