diff --git a/chfs-ng/cmd/chfs/main.go b/chfs-ng/cmd/chfs/main.go index a02df5d..cd1a832 100644 --- a/chfs-ng/cmd/chfs/main.go +++ b/chfs-ng/cmd/chfs/main.go @@ -67,9 +67,9 @@ func main() { if len(cfg.Path) == 1 { 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) - 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 != "" { log.Fatal(http.ListenAndServeTLS(addr, cfg.SSLCert, cfg.SSLKey, srv.Handler())) } diff --git a/chfs-ng/go.mod b/chfs-ng/go.mod index dc99991..e734f68 100644 --- a/chfs-ng/go.mod +++ b/chfs-ng/go.mod @@ -2,4 +2,9 @@ module chfs-ng 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 diff --git a/chfs-ng/go.sum b/chfs-ng/go.sum index bec93c6..7dec236 100644 --- a/chfs-ng/go.sum +++ b/chfs-ng/go.sum @@ -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-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/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= 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= diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 61d186f..548051d 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -8,6 +8,7 @@ import ( "mime/multipart" "net" "net/http" + "os" "path" "path/filepath" "strings" @@ -25,6 +26,7 @@ type Server struct { WebDAVRoot string Allow string MultiRoot bool + RemoveMode int } type fileInfo struct { 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) { a := account(r, s) + _ = r.ParseMultipartForm(16 << 20) _ = r.ParseForm() items := r.Form["filepath"] if len(items) == 0 { @@ -166,13 +169,38 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) { http.Error(w, "forbidden", 403) return } - if e := s.Store.Remove(p); e != nil { + if e := s.removePath(p); e != nil { http.Error(w, e.Error(), 500) return } } 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) { p := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/") _, e := s.Store.Stat(p) diff --git a/chfs-ng/internal/auth/auth.go b/chfs-ng/internal/auth/auth.go index 8c003ba..9a74e92 100644 --- a/chfs-ng/internal/auth/auth.go +++ b/chfs-ng/internal/auth/auth.go @@ -71,7 +71,7 @@ func ParseRules(values []string) (*Manager, error) { } } if m.Guest == nil { - m.Guest = &Account{Mask: Read | Write} + m.Guest = &Account{Mask: Read | Write | Delete} } return m, nil } diff --git a/chfs-ng/internal/storage/multipath.go b/chfs-ng/internal/storage/multipath.go index 6748232..f0ec2b4 100644 --- a/chfs-ng/internal/storage/multipath.go +++ b/chfs-ng/internal/storage/multipath.go @@ -91,6 +91,31 @@ func (m Multi) Remove(p string) error { } 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 { ra, aa, oka := m.find(a) rb, bb, okb := m.find(b) diff --git a/chfs-ng/internal/storage/storage.go b/chfs-ng/internal/storage/storage.go index b018af7..809265d 100644 --- a/chfs-ng/internal/storage/storage.go +++ b/chfs-ng/internal/storage/storage.go @@ -2,12 +2,20 @@ package storage import ( "errors" + "fmt" "io/fs" "os" "path/filepath" "strings" + + "github.com/Bios-Marcel/wastebasket" ) +type Trashable interface { + Trash(path string) error + MoveToTrash(path, trashRoot string) error +} + type Storage interface { Stat(path string) (os.FileInfo, error) Open(path string) (*os.File, error) @@ -67,6 +75,33 @@ func (s Local) Remove(path string) error { } 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 { pa, e := s.real(a) if e != nil {