2025-07-27 14:26:30 +00:00
|
|
|
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"
|
|
|
|
|
|
2025-08-18 19:50:34 +00:00
|
|
|
// --- 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/*) ---
|
2025-07-27 14:26:30 +00:00
|
|
|
rootEntries, err := listEntries(base, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list root entries"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 19:50:34 +00:00
|
|
|
// On ne garde que les dossiers et on prépare la liste des noms pour une recherche en BDD
|
2025-07-27 14:26:30 +00:00
|
|
|
var dirs []Entry
|
2025-08-18 19:50:34 +00:00
|
|
|
var rootNames []string
|
2025-07-27 14:26:30 +00:00
|
|
|
for _, e := range rootEntries {
|
|
|
|
|
if e.IsDir {
|
|
|
|
|
dirs = append(dirs, e)
|
2025-08-18 19:50:34 +00:00
|
|
|
rootNames = append(rootNames, e.Name)
|
2025-07-27 14:26:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 19:50:34 +00:00
|
|
|
// 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/<cur>) ---
|
2025-07-27 14:26:30 +00:00
|
|
|
entries, err := listEntries(base, cur)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list entries"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 19:50:34 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|