feat: add permission-aware file copy and move

This commit is contained in:
OpenClaw Agent
2026-09-22 02:21:07 +08:00
parent b60d4dc3cb
commit 0ddb04b551
3 changed files with 107 additions and 0 deletions
+81
View File
@@ -73,6 +73,8 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("/chfs/files", s.files) mux.HandleFunc("/chfs/files", s.files)
mux.HandleFunc("/chfs/newdir", s.newdir) mux.HandleFunc("/chfs/newdir", s.newdir)
mux.HandleFunc("/chfs/rename", s.rename) mux.HandleFunc("/chfs/rename", s.rename)
mux.HandleFunc("/chfs/move", s.move)
mux.HandleFunc("/chfs/copy", s.copy)
mux.HandleFunc("/chfs/rmfiles", s.remove) mux.HandleFunc("/chfs/rmfiles", s.remove)
mux.HandleFunc("/chfs/exist", s.exist) mux.HandleFunc("/chfs/exist", s.exist)
mux.HandleFunc("/chfs/session", s.session) mux.HandleFunc("/chfs/session", s.session)
@@ -345,6 +347,85 @@ func (s *Server) rename(w http.ResponseWriter, r *http.Request) {
s.logOp(r, fmt.Sprintf("renamed '%s' to '%s'", old, neu)) s.logOp(r, fmt.Sprintf("renamed '%s' to '%s'", old, neu))
writeCreated(w) writeCreated(w)
} }
func (s *Server) transfer(w http.ResponseWriter, r *http.Request, copyOnly bool) {
a := account(r, s)
rawSrc, rawDst := r.FormValue("source"), r.FormValue("destination")
if unsafePath(rawSrc) || unsafePath(rawDst) {
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
return
}
src, dst := strings.Trim(rawSrc, "/"), strings.Trim(rawDst, "/")
if src == "" || dst == "" || src == dst {
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
return
}
if !a.MaskFor(src).HasRead() || !a.MaskFor(dst).HasWrite() || !a.MaskFor(path.Dir(dst)).HasWrite() {
writeJSONError(w, http.StatusForbidden, "has no privilege")
return
}
info, err := s.Store.Stat(src)
if err != nil {
http.Error(w, err.Error(), 404)
return
}
if info.IsDir() && (dst == src || strings.HasPrefix(dst, src+"/")) {
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
return
}
if _, err = s.Store.Stat(dst); err == nil {
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
return
} else if !os.IsNotExist(err) {
http.Error(w, err.Error(), 500)
return
}
if info.IsDir() {
if err = checkReadTree(s.Store, a, src); err != nil {
writeJSONError(w, http.StatusForbidden, err.Error())
return
}
}
if copyOnly {
err = s.Store.Copy(src, dst)
} else {
err = s.Store.Rename(src, dst)
}
if err != nil {
http.Error(w, err.Error(), 500)
return
}
action := "moved"
if copyOnly {
action = "copied"
}
s.logOp(r, fmt.Sprintf("%s '%s' to '%s'", action, src, dst))
writeCreated(w)
}
func checkReadTree(store storage.Storage, a *auth.Account, root string) error {
if !a.MaskFor(root).HasRead() {
return fmt.Errorf("read permission denied")
}
info, err := store.Stat(root)
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
items, err := store.List(root)
if err != nil {
return err
}
for _, item := range items {
if err := checkReadTree(store, a, path.Join(root, item.Name())); err != nil {
return err
}
}
return nil
}
func (s *Server) move(w http.ResponseWriter, r *http.Request) { s.transfer(w, r, false) }
func (s *Server) copy(w http.ResponseWriter, r *http.Request) { s.transfer(w, r, true) }
func (s *Server) remove(w http.ResponseWriter, r *http.Request) { func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
a := account(r, s) a := account(r, s)
_ = r.ParseMultipartForm(16 << 20) _ = r.ParseMultipartForm(16 << 20)
+8
View File
@@ -147,6 +147,14 @@ func (m Multi) RootName(r Local) string {
} }
return "root" return "root"
} }
func (m Multi) Copy(a, b string) error {
ra, aa, oka := m.find(a)
rb, bb, okb := m.find(b)
if !oka || !okb || aa == "" || bb == "" || ra.Root != rb.Root {
return os.ErrInvalid
}
return ra.Copy(aa, bb)
}
func (m Multi) Rename(a, b string) error { func (m Multi) Rename(a, b string) error {
ra, aa, oka := m.find(a) ra, aa, oka := m.find(a)
rb, bb, okb := m.find(b) rb, bb, okb := m.find(b)
+18
View File
@@ -26,6 +26,7 @@ type Storage interface {
Create(path string) (*os.File, error) Create(path string) (*os.File, error)
Remove(path string) error Remove(path string) error
Rename(oldPath, newPath string) error Rename(oldPath, newPath string) error
Copy(oldPath, newPath string) error
MkdirAll(path string) error MkdirAll(path string) error
List(path string) ([]os.FileInfo, error) List(path string) ([]os.FileInfo, error)
} }
@@ -275,6 +276,23 @@ func copyPath(src, dst string) error {
return os.Chtimes(dst, info.ModTime(), info.ModTime()) return os.Chtimes(dst, info.ModTime(), info.ModTime())
} }
func (s Local) Copy(a, b string) error {
pa, e := s.real(a)
if e != nil {
return e
}
pb, e := s.real(b)
if e != nil {
return e
}
if _, e := os.Lstat(pb); e == nil {
return os.ErrExist
} else if !os.IsNotExist(e) {
return e
}
return copyPath(pa, pb)
}
func (s Local) Rename(a, b string) error { func (s Local) Rename(a, b string) error {
pa, e := s.real(a) pa, e := s.real(a)
if e != nil { if e != nil {