package controllers import ( "canguidev/shelfy/internal/models" "net/http" "net/url" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type Entry struct { ID int64 `json:"ID"` Name, Path string IsDir bool ModTime time.Time Size int64 Children []Entry `json:"Children,omitempty"` Url string `json:"Url,omitempty"` // <- Ajout ici } func listEntries(base, rel string) ([]Entry, error) { dir := filepath.Join(base, rel) fis, err := os.ReadDir(dir) if err != nil { return nil, err } out := make([]Entry, 0, len(fis)) for _, fi := range fis { info, _ := fi.Info() out = append(out, Entry{ Name: fi.Name(), Path: filepath.ToSlash(filepath.Join(rel, fi.Name())), IsDir: fi.IsDir(), ModTime: info.ModTime(), Size: info.Size(), }) } return out, nil } func StreamHandler(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { base := "upload" // --- Sanitize du chemin courant --- raw := c.Query("path") cur := filepath.Clean("/" + strings.TrimSpace(raw)) // force un chemin absolu “virtuel” if cur == "/" { cur = "" // racine logique sous "upload" } // Interdit les traversées ou chemins absolus réels if strings.Contains(cur, "..") || strings.HasPrefix(raw, "/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Chemin invalide"}) return } // --- Liste des dossiers du dossier racine (upload/*) --- rootEntries, err := listEntries(base, "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list root entries"}) return } // On ne garde que les dossiers et on prépare la liste des noms pour une recherche en BDD var dirs []Entry var rootNames []string for _, e := range rootEntries { if e.IsDir { dirs = append(dirs, e) rootNames = append(rootNames, e.Name) } } // Batch: récupère les IDs pour tous les noms racine (Path = nom simple) // (tu peux remplacer "path = ?" par "path_name = ?" si tu préfères) var rows []models.PathDownload if len(rootNames) > 0 { if err := db.Where("path IN ?", rootNames).Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "DB lookup failed"}) return } } // Map nom -> ID (int64) idByName := make(map[string]int64, len(rows)) for _, r := range rows { idByName[r.Path] = r.ID // Path = "Film"/"Série"/... } // Affecte l'ID seulement pour les dossiers racine for i := range dirs { if id, ok := idByName[dirs[i].Name]; ok { dirs[i].ID = id } else { dirs[i].ID = 0 // non trouvé en BDD } } // --- Liste du chemin courant (upload/) --- entries, err := listEntries(base, cur) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list entries"}) return } // IMPORTANT : // La BDD ne contient que les dossiers racine. // Donc on ne tente de mettre un ID aux entries QUE si on est à la racine. if cur == "" && len(entries) > 0 { var names []string for _, e := range entries { if e.IsDir { names = append(names, e.Name) } } var rows2 []models.PathDownload if len(names) > 0 { if err := db.Where("path IN ?", names).Find(&rows2).Error; err == nil { tmp := make(map[string]int64, len(rows2)) for _, r := range rows2 { tmp[r.Path] = r.ID } for i := range entries { if entries[i].IsDir { if id, ok := tmp[entries[i].Name]; ok { entries[i].ID = id } } } } } } else { // On laisse ID = 0 pour le contenu interne des catégories (non suivi en BDD) for i := range entries { entries[i].ID = 0 } } c.JSON(http.StatusOK, gin.H{ "dirs": dirs, "entries": entries, "currentPath": cur, }) } } func DetailHandler() gin.HandlerFunc { return func(c *gin.Context) { base := "upload" rel := c.Query("path") rel = filepath.Clean("/" + rel) rel = strings.TrimPrefix(rel, "/") // => chemin relatif à "upload" absPath := filepath.Join(base, rel) absPath, err := filepath.Abs(absPath) if err != nil { c.JSON(500, gin.H{"error": "Path error"}) return } baseAbs, err := filepath.Abs(base) if err != nil { c.JSON(500, gin.H{"error": "Base path error"}) return } if !strings.HasPrefix(absPath, baseAbs) { c.JSON(403, gin.H{"error": "Access outside base directory is forbidden"}) return } info, err := os.Stat(absPath) if err != nil { c.JSON(404, gin.H{"error": "Path not found"}) return } type Entry struct { ID int64 `json:"ID,omitempty"` Name string `json:"Name"` Path string `json:"Path"` // <--- Toujours RELATIF à "upload" IsDir bool `json:"IsDir"` ModTime time.Time `json:"ModTime"` Size int64 `json:"Size"` Children []Entry `json:"Children,omitempty"` Url string `json:"Url,omitempty"` // <--- Lien d'accès au fichier } var buildTree func(absPath, relPath string, fi os.FileInfo) (Entry, error) buildTree = func(absPath, relPath string, fi os.FileInfo) (Entry, error) { entry := Entry{ Name: fi.Name(), Path: relPath, // <-- toujours RELATIF à la racine upload IsDir: fi.IsDir(), ModTime: fi.ModTime(), Size: fi.Size(), } if !fi.IsDir() { entry.Url = "/api/v1/folders/file?path=" + url.QueryEscape(relPath) } if fi.IsDir() { f, err := os.Open(absPath) if err != nil { return entry, err } defer f.Close() files, err := f.Readdir(0) if err != nil { return entry, err } for _, child := range files { childAbs := filepath.Join(absPath, child.Name()) childRel := filepath.Join(relPath, child.Name()) childEntry, err := buildTree(childAbs, childRel, child) if err != nil { continue } entry.Children = append(entry.Children, childEntry) } } return entry, nil } rootEntry, err := buildTree(absPath, rel, info) if err != nil { c.JSON(500, gin.H{"error": "Tree error"}) return } c.JSON(200, rootEntry) } }