Init and optimizations
This commit is contained in:
150
racc_filebrowser/racc_filebrowser.go
Normal file
150
racc_filebrowser/racc_filebrowser.go
Normal 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 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user