Add checkbox folder browsers
This commit is contained in:
@@ -36,6 +36,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server {
|
||||
mux.HandleFunc("PUT /v1/secrets/{id}", s.putSecret)
|
||||
mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret)
|
||||
mux.HandleFunc("GET /v1/runs", s.runs)
|
||||
mux.HandleFunc("GET /v1/filesystem/directories", s.directories)
|
||||
mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob)
|
||||
mux.HandleFunc("POST /v1/runs/{id}/cancel", s.cancelRun)
|
||||
mux.HandleFunc("POST /v1/repositories/{id}/test", s.testRepository)
|
||||
@@ -116,6 +117,14 @@ func (s *Server) deleteSecret(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
func (s *Server) runs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, s.service.Runs()) }
|
||||
func (s *Server) directories(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := s.service.BrowseDirectories(r.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, items)
|
||||
}
|
||||
|
||||
func (s *Server) runJob(w http.ResponseWriter, r *http.Request) {
|
||||
run, err := s.service.EnqueueJob(r.PathValue("id"), 10)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DirectoryEntry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
var browseRoots = []string{"/mnt/user", "/mnt/disks", "/mnt/remotes", "/boot"}
|
||||
|
||||
func BrowseDirectories(path string) ([]DirectoryEntry, error) {
|
||||
clean, err := validateBrowsePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(clean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]DirectoryEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && entry.Type()&os.ModeSymlink == 0 {
|
||||
result = append(result, DirectoryEntry{Name: entry.Name(), Path: filepath.Join(clean, entry.Name())})
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return strings.ToLower(result[i].Name) < strings.ToLower(result[j].Name) })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func BrowseRoots() []DirectoryEntry {
|
||||
result := make([]DirectoryEntry, 0, len(browseRoots))
|
||||
for _, root := range browseRoots {
|
||||
if info, err := os.Stat(root); err == nil && info.IsDir() {
|
||||
result = append(result, DirectoryEntry{Name: root, Path: root})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validateBrowsePath(path string) (string, error) {
|
||||
if path == "" || path == "/" {
|
||||
return "", errors.New("validation: select an allowed browse root")
|
||||
}
|
||||
clean := filepath.Clean(path)
|
||||
if !filepath.IsAbs(clean) {
|
||||
return "", errors.New("validation: browse path must be absolute")
|
||||
}
|
||||
allowed := false
|
||||
for _, root := range browseRoots {
|
||||
if clean == root || strings.HasPrefix(clean, root+string(os.PathSeparator)) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return "", errors.New("validation: browse path is outside allowed Unraid storage roots")
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(clean)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, root := range browseRoots {
|
||||
if resolved == root || strings.HasPrefix(resolved, root+string(os.PathSeparator)) {
|
||||
return resolved, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("validation: browse path resolves outside allowed Unraid storage roots")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package platform
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateBrowsePathRejectsUnsafePaths(t *testing.T) {
|
||||
for _, path := range []string{"", "/", "/etc", "/mnt/user/../../etc", "relative"} {
|
||||
if _, err := validateBrowsePath(path); err == nil {
|
||||
t.Fatalf("unsafe browse path accepted: %q", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,13 @@ func (s *Service) Stop() { s.queue.Stop() }
|
||||
func (s *Service) Config() model.Config { s.mu.RLock(); defer s.mu.RUnlock(); return s.config }
|
||||
func (s *Service) Runs() []model.Run { return s.queue.Snapshot() }
|
||||
|
||||
func (s *Service) BrowseDirectories(path string) ([]platform.DirectoryEntry, error) {
|
||||
if path == "" || path == "/" {
|
||||
return platform.BrowseRoots(), nil
|
||||
}
|
||||
return platform.BrowseDirectories(path)
|
||||
}
|
||||
|
||||
func (s *Service) LastRun(jobID string) time.Time {
|
||||
var latest time.Time
|
||||
for _, run := range s.queue.Snapshot() {
|
||||
|
||||
Reference in New Issue
Block a user