151 lines
3.6 KiB
Go
151 lines
3.6 KiB
Go
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 Wurzelordner
|
||
)
|
||
|
||
// ---------- 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 Hauptserver 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)
|
||
}
|
||
}
|