Init and optimizations

This commit is contained in:
2025-07-12 23:09:54 +02:00
parent 1a0b081e96
commit 77e4a2b6d1
11 changed files with 1061 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
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)
}
}