pegasus/backend/handlers_fs.go

276 lines
7.0 KiB
Go
Raw Normal View History

2026-04-11 00:02:59 -03:00
// backend/handlers_fs.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"scm.bstein.dev/bstein/Pegasus/backend/internal"
)
type listEntry struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"is_dir"`
Size int64 `json:"size"`
Mtime int64 `json:"mtime"`
}
func resolveLibraryRoot(um *internal.UserMap, username, requested string) (string, error) {
allRoots, _ := um.ResolveAll(username)
reqLib := sanitizeSegment(requested)
if reqLib != "" && contains(allRoots, reqLib) {
return reqLib, nil
}
return um.Resolve(username)
}
func listHandler(um *internal.UserMap) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cl, err := internal.CurrentUser(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
rootRel, err := resolveLibraryRoot(um, cl.Username, r.URL.Query().Get("lib"))
if err != nil {
internal.Logf("list: map missing for %q", cl.Username)
http.Error(w, "no mapping", http.StatusForbidden)
return
}
q := sanitizeSegment(r.URL.Query().Get("path"))
rootAbs, _ := internal.SafeJoin(mediaRoot, rootRel)
dirAbs := rootAbs
if q != "" {
if dirAbs, err = internal.SafeJoin(rootAbs, q); err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
}
ents, err := os.ReadDir(dirAbs)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
out := make([]listEntry, 0, len(ents))
for _, d := range ents {
info, _ := d.Info()
var size int64
if info != nil && !d.IsDir() {
size = info.Size()
}
var mtime int64
if info != nil {
mtime = info.ModTime().Unix()
}
out = append(out, listEntry{
Name: d.Name(),
Path: filepath.Join(q, d.Name()),
IsDir: d.IsDir(),
Size: size,
Mtime: mtime,
})
}
sort.Slice(out, func(i, j int) bool {
if out[i].IsDir != out[j].IsDir {
return out[i].IsDir
}
return out[i].Name < out[j].Name
})
writeJSON(w, out)
}
}
func renameHandler(um *internal.UserMap, jf jellyfinClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cl, err := internal.CurrentUser(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var p struct {
Lib string `json:"lib"`
From string `json:"from"`
To string `json:"to"`
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
if filepath.Ext(p.To) != "" && !finalNameRe.MatchString(filepath.Base(p.To)) {
http.Error(w, "new name must match YYYY.MM.DD.Description.ext", http.StatusBadRequest)
return
}
rootRel, err := resolveLibraryRoot(um, cl.Username, p.Lib)
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
rootAbs, _ := internal.SafeJoin(mediaRoot, rootRel)
fromAbs, err := internal.SafeJoin(rootAbs, strings.TrimPrefix(p.From, "/"))
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
toAbs, err := internal.SafeJoin(rootAbs, strings.TrimPrefix(p.To, "/"))
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
_ = os.MkdirAll(filepath.Dir(toAbs), 0o2775)
if internal.DryRun {
internal.Logf("[DRY] mv %s -> %s", fromAbs, toAbs)
} else if err := os.Rename(fromAbs, toAbs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jf.RefreshLibrary(cl.JFToken)
writeJSON(w, map[string]any{"ok": true})
}
}
func deleteHandler(um *internal.UserMap, jf jellyfinClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cl, err := internal.CurrentUser(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
rootRel, err := resolveLibraryRoot(um, cl.Username, r.URL.Query().Get("lib"))
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
path := strings.TrimPrefix(r.URL.Query().Get("path"), "/")
rec := r.URL.Query().Get("recursive") == "true"
rootAbs, _ := internal.SafeJoin(mediaRoot, rootRel)
abs, err := internal.SafeJoin(rootAbs, path)
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if internal.DryRun {
internal.Logf("[DRY] rm%s %s", map[bool]string{true: " -r", false: ""}[rec], abs)
} else if rec {
err = os.RemoveAll(abs)
} else {
err = os.Remove(abs)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jf.RefreshLibrary(cl.JFToken)
writeJSON(w, map[string]any{"ok": true})
}
}
func mkdirHandler(um *internal.UserMap, jf jellyfinClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cl, err := internal.CurrentUser(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var p struct {
Lib string `json:"lib"`
Path string `json:"path"`
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
rootRel, err := resolveLibraryRoot(um, cl.Username, p.Lib)
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
rootAbs, _ := internal.SafeJoin(mediaRoot, rootRel)
if fi, err := os.Stat(rootAbs); err != nil || !fi.IsDir() {
http.Error(w, "library not found", http.StatusBadRequest)
return
}
seg := sanitizeSegment(p.Path)
if seg == "" {
http.Error(w, "folder name is empty", http.StatusBadRequest)
return
}
abs, err := internal.SafeJoin(rootAbs, seg)
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if internal.DryRun {
internal.Logf("[DRY] mkdir -p %s", abs)
} else if err := os.MkdirAll(abs, 0o2775); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jf.RefreshLibrary(cl.JFToken)
writeJSON(w, map[string]any{"ok": true})
}
}
func debugEnvHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !internal.Debug {
http.Error(w, "disabled", http.StatusForbidden)
return
}
writeJSON(w, map[string]any{
"mediaRoot": mediaRoot,
"tusDir": tusDir,
"userMapFile": userMapFile,
})
}
}
func debugWriteTestHandler(um *internal.UserMap) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !internal.Debug {
http.Error(w, "disabled", http.StatusForbidden)
return
}
cl, err := internal.CurrentUser(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
rootRel, err := um.Resolve(cl.Username)
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
rootAbs, _ := internal.SafeJoin(mediaRoot, rootRel)
test := filepath.Join(rootAbs, fmt.Sprintf("TEST.%d.txt", time.Now().Unix()))
if internal.DryRun {
internal.Logf("[DRY] write %s", test)
} else {
_ = os.WriteFile(test, []byte("ok\n"), 0o644)
}
writeJSON(w, map[string]string{"wrote": test})
}
}