feat: implement configurable delete modes

This commit is contained in:
OpenClaw Agent
2026-09-21 13:38:18 +08:00
parent 633c833d1c
commit add2a22416
7 changed files with 102 additions and 5 deletions
+2 -2
View File
@@ -67,9 +67,9 @@ func main() {
if len(cfg.Path) == 1 { if len(cfg.Path) == 1 {
store = storage.Local{Root: root} store = storage.Local{Root: root}
} }
srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1} srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1, RemoveMode: cfg.FileRemove}
addr := ":" + itoa(cfg.Port) addr := ":" + itoa(cfg.Port)
log.Printf("chfs-ng listening on %s root=%s", addr, filepath.Clean(root)) log.Printf("chfs-ng listening on %s root=%s removeMode=%d", addr, filepath.Clean(root), cfg.FileRemove)
if cfg.SSLCert != "" && cfg.SSLKey != "" { if cfg.SSLCert != "" && cfg.SSLKey != "" {
log.Fatal(http.ListenAndServeTLS(addr, cfg.SSLCert, cfg.SSLKey, srv.Handler())) log.Fatal(http.ListenAndServeTLS(addr, cfg.SSLCert, cfg.SSLKey, srv.Handler()))
} }
+6 -1
View File
@@ -2,4 +2,9 @@ module chfs-ng
go 1.22 go 1.22
require github.com/emersion/go-webdav v0.7.0 require (
github.com/Bios-Marcel/wastebasket v0.0.3
github.com/emersion/go-webdav v0.7.0
)
require golang.org/x/sys v0.3.0 // indirect
+4
View File
@@ -1,5 +1,9 @@
github.com/Bios-Marcel/wastebasket v0.0.3 h1:gyuMcoDav6n+sdynn9TDl1NN7GOAnoy227JErVSW3RM=
github.com/Bios-Marcel/wastebasket v0.0.3/go.mod h1:FChzXi1izqzdPb6BiNZmcZLGyTYiT61iGx9Rxx9GNeI=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA= github.com/emersion/go-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA=
github.com/emersion/go-webdav v0.7.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= github.com/emersion/go-webdav v0.7.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+29 -1
View File
@@ -8,6 +8,7 @@ import (
"mime/multipart" "mime/multipart"
"net" "net"
"net/http" "net/http"
"os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -25,6 +26,7 @@ type Server struct {
WebDAVRoot string WebDAVRoot string
Allow string Allow string
MultiRoot bool MultiRoot bool
RemoveMode int
} }
type fileInfo struct { type fileInfo struct {
Name string `json:"name"` Name string `json:"name"`
@@ -156,6 +158,7 @@ func (s *Server) rename(w http.ResponseWriter, r *http.Request) {
} }
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.ParseForm() _ = r.ParseForm()
items := r.Form["filepath"] items := r.Form["filepath"]
if len(items) == 0 { if len(items) == 0 {
@@ -166,13 +169,38 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
http.Error(w, "forbidden", 403) http.Error(w, "forbidden", 403)
return return
} }
if e := s.Store.Remove(p); e != nil { if e := s.removePath(p); e != nil {
http.Error(w, e.Error(), 500) http.Error(w, e.Error(), 500)
return return
} }
} }
writeJSON(w, map[string]any{"ok": true}) writeJSON(w, map[string]any{"ok": true})
} }
func (s *Server) removePath(p string) error {
mode := s.RemoveMode
if mode == 0 {
mode = 1
}
t, ok := s.Store.(storage.Trashable)
switch mode {
case 1:
if !ok {
return fmt.Errorf("storage does not support system trash")
}
return t.Trash(p)
case 2:
home, err := os.UserHomeDir()
if err != nil {
return err
}
return t.MoveToTrash(p, filepath.Join(home, ".chfs_trashbin"))
case 3:
return s.Store.Remove(p)
default:
return fmt.Errorf("invalid file.remove mode: %d", mode)
}
}
func (s *Server) exist(w http.ResponseWriter, r *http.Request) { func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
p := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/") p := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/")
_, e := s.Store.Stat(p) _, e := s.Store.Stat(p)
+1 -1
View File
@@ -71,7 +71,7 @@ func ParseRules(values []string) (*Manager, error) {
} }
} }
if m.Guest == nil { if m.Guest == nil {
m.Guest = &Account{Mask: Read | Write} m.Guest = &Account{Mask: Read | Write | Delete}
} }
return m, nil return m, nil
} }
+25
View File
@@ -91,6 +91,31 @@ func (m Multi) Remove(p string) error {
} }
return r.Remove(rel) return r.Remove(rel)
} }
func (m Multi) Trash(p string) error {
r, rel, ok := m.find(p)
if !ok {
return os.ErrNotExist
}
return r.Trash(rel)
}
func (m Multi) MoveToTrash(p, trashRoot string) error {
r, rel, ok := m.find(p)
if !ok {
return os.ErrNotExist
}
return r.MoveToTrash(rel, filepath.Join(trashRoot, m.RootName(r)))
}
func (m Multi) RootName(r Local) string {
for i := range m.Roots {
if m.Roots[i].Root == r.Root {
return m.Names[i]
}
}
return "root"
}
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)
+35
View File
@@ -2,12 +2,20 @@ package storage
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/Bios-Marcel/wastebasket"
) )
type Trashable interface {
Trash(path string) error
MoveToTrash(path, trashRoot string) error
}
type Storage interface { type Storage interface {
Stat(path string) (os.FileInfo, error) Stat(path string) (os.FileInfo, error)
Open(path string) (*os.File, error) Open(path string) (*os.File, error)
@@ -67,6 +75,33 @@ func (s Local) Remove(path string) error {
} }
return os.RemoveAll(p) return os.RemoveAll(p)
} }
func (s Local) Trash(path string) error {
p, e := s.real(path)
if e != nil {
return e
}
return wastebasket.Trash(p)
}
func (s Local) MoveToTrash(path, trashRoot string) error {
p, e := s.real(path)
if e != nil {
return e
}
if e = os.MkdirAll(trashRoot, 0755); e != nil {
return e
}
name := filepath.Base(p)
target := filepath.Join(trashRoot, name)
for i := 1; ; i++ {
if _, e = os.Lstat(target); os.IsNotExist(e) {
break
}
target = filepath.Join(trashRoot, fmt.Sprintf("%s.%d", name, i))
}
return os.Rename(p, target)
}
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 {