Files
raccws/racc_filebrowser/racc_filebrowser.go
2025-07-12 23:09:54 +02:00

151 lines
3.6 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}