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/newdir", s.newdir)
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/exist", s.exist)
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))
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) {
a := account(r, s)
_ = r.ParseMultipartForm(16 << 20)