package racc_filebrowser import ( "errors" "html/template" "net/http" "os" "path" "path/filepath" "sort" "strings" ) const ( appID = "files" baseURI = "/files" // URL-Prefix (immer Slash) root = "./" + appID // lokaler Wurzel­ordner ) // ---------- Helper-Typen ---------- type fileInfo struct { Name string // nur Dateiname WebPath string // URL-Pfad (Slash-Separators) LocalPath string // voller lokaler Pfad (OS-Separators) IsDir bool } type pageData struct { Path string // angezeigter Ordner (URL-Pfad ab /files) Query string // Suchbegriff Dirs []fileInfo // nur Ordner Files []fileInfo // nur Dateien } // ---------- Öffentliche API ---------- // Router liefert eine fertige Sub-Mux, die du im Haupt­server mounten kannst. func Router() *http.ServeMux { m := http.NewServeMux() m.HandleFunc("/", handle) return m } // ---------- HTTP-Handler ---------- func handle(w http.ResponseWriter, r *http.Request) { // Zielpfad aus URL zusammensetzen und absichern reqPath := strings.TrimPrefix(r.URL.Path, baseURI) // »/foo« → »foo« local, err := safeJoin(root, reqPath) // verhindert ..-Traversal if err != nil { http.Error(w, "invalid path", http.StatusBadRequest) return } query := r.URL.Query().Get("q") // Datei-Info ermitteln info, err := os.Stat(local) if errors.Is(err, os.ErrNotExist) { http.NotFound(w, r) return } else if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Datei direkt ausliefern if !info.IsDir() && query == "" { http.ServeFile(w, r, local) return } // Verzeichnis auflisten dirs, files, err := listDir(local, query) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } pd := pageData{ Path: path.Clean("/" + reqPath), // immer URL-Slash Query: query, Dirs: dirs, Files: files, } renderTemplate(w, pd) } // ---------- Pfad-Utilities ---------- // safeJoin verknüpft root und req so, dass das Ergebnis IMMER unter root bleibt. func safeJoin(root, req string) (string, error) { clean := filepath.Clean("/" + req) // Normieren local := filepath.Join(root, clean) // OS-Pfad if !strings.HasPrefix(local, filepath.Clean(root)) { return "", errors.New("path escape") } return local, nil } // buildWebPath konvertiert einen lokalen Pfad zurück in einen URL-Pfad. func buildWebPath(local string) string { rel, _ := filepath.Rel(root, local) // garantiert unter root return path.Join(baseURI, filepath.ToSlash(rel)) } // ---------- Verzeichnis lesen ---------- func listDir(folder, query string) (dirs, files []fileInfo, err error) { entries, err := os.ReadDir(folder) if err != nil { return nil, nil, err } for _, e := range entries { name := e.Name() if query != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(query)) { continue } local := filepath.Join(folder, name) item := fileInfo{ Name: name, LocalPath: local, WebPath: buildWebPath(local), IsDir: e.IsDir(), } if item.IsDir { dirs = append(dirs, item) } else { files = append(files, item) } } // alphabetisch sortieren sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name < dirs[j].Name }) sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name }) return } // ---------- Template-Renderer ---------- var tmpl = template.Must(template.ParseFiles("./html-templates/filebrowser.html")) func renderTemplate(w http.ResponseWriter, pd pageData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.Execute(w, pd); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }