276 lines
7.0 KiB
Go
276 lines
7.0 KiB
Go
|
|
// 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})
|
||
|
|
}
|
||
|
|
}
|