refactor: route WebDAV through storage backend
This commit is contained in:
@@ -606,18 +606,203 @@ func (s *Server) webdav(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = "/" + vp
|
||||
defer func() { r.URL.Path = old }()
|
||||
inner := s.WebDAVFS
|
||||
if inner == nil && s.MultiRoot {
|
||||
if m, ok := s.Store.(storage.Multi); ok {
|
||||
inner = newWebDAVMultiFS(m)
|
||||
}
|
||||
}
|
||||
if inner == nil {
|
||||
inner = webdav.LocalFileSystem(s.WebDAVRoot)
|
||||
inner = &webdavStorageFS{store: s.Store, removeMode: s.RemoveMode}
|
||||
}
|
||||
fs := &webdavAuthFS{inner: inner, account: account(r, s)}
|
||||
(&webdav.Handler{FileSystem: fs}).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type webdavStorageFS struct {
|
||||
store storage.Storage
|
||||
removeMode int
|
||||
}
|
||||
|
||||
func davStoragePath(name string) string {
|
||||
p := strings.Trim(strings.ReplaceAll(name, "\\", "/"), "/")
|
||||
p = strings.TrimPrefix(p, "webdav/")
|
||||
p = strings.TrimPrefix(p, "webdav")
|
||||
return strings.Trim(p, "/")
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
|
||||
p := davStoragePath(name)
|
||||
fi, err := f.store.Stat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &webdav.FileInfo{Path: "/" + p, Size: fi.Size(), ModTime: fi.ModTime(), IsDir: fi.IsDir()}, nil
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
p := davStoragePath(name)
|
||||
fi, err := f.store.Stat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return nil, fmt.Errorf("is a directory")
|
||||
}
|
||||
return f.store.Open(p)
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
|
||||
return f.info(name)
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
||||
root := davStoragePath(name)
|
||||
var out []webdav.FileInfo
|
||||
var walk func(string) error
|
||||
walk = func(dir string) error {
|
||||
items, err := f.store.List(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
p := path.Join(dir, item.Name())
|
||||
out = append(out, webdav.FileInfo{Path: "/" + p, Size: item.Size(), ModTime: item.ModTime(), IsDir: item.IsDir()})
|
||||
if recursive && item.IsDir() {
|
||||
if err := walk(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := walk(root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
||||
p := davStoragePath(name)
|
||||
_, statErr := f.store.Stat(p)
|
||||
if statErr != nil && !os.IsNotExist(statErr) {
|
||||
return nil, false, statErr
|
||||
}
|
||||
created := os.IsNotExist(statErr)
|
||||
file, err := f.store.Create(p)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
_, copyErr := io.Copy(file, body)
|
||||
closeErr := file.Close()
|
||||
body.Close()
|
||||
if copyErr != nil {
|
||||
return nil, false, copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, false, closeErr
|
||||
}
|
||||
fi, err := f.info(p)
|
||||
return fi, created, err
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
||||
p := davStoragePath(name)
|
||||
if p == "" {
|
||||
return fmt.Errorf("cannot remove storage root")
|
||||
}
|
||||
if f.removeMode == 1 {
|
||||
if t, ok := f.store.(storage.Trashable); ok {
|
||||
return t.Trash(p)
|
||||
}
|
||||
}
|
||||
if f.removeMode == 2 {
|
||||
if t, ok := f.store.(storage.Trashable); ok {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.MoveToTrash(p, filepath.Join(home, ".chfs_trashbin"))
|
||||
}
|
||||
}
|
||||
return f.store.Remove(p)
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) Mkdir(ctx context.Context, name string) error {
|
||||
p := davStoragePath(name)
|
||||
if p == "" {
|
||||
return fmt.Errorf("cannot create storage root")
|
||||
}
|
||||
return f.store.MkdirAll(p)
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
||||
src := davStoragePath(name)
|
||||
dst := davStoragePath(dest)
|
||||
if m, ok := f.store.(storage.Multi); ok {
|
||||
srcRoot, _, srcOK := m.FindRoot(src)
|
||||
dstRoot, _, dstOK := m.FindRoot(dst)
|
||||
if srcOK && dstOK && srcRoot != dstRoot {
|
||||
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root copy is not supported"))
|
||||
}
|
||||
}
|
||||
info, err := f.store.Stat(src)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
if err := f.store.MkdirAll(dst); err != nil {
|
||||
return false, err
|
||||
}
|
||||
items, err := f.store.List(src)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, item := range items {
|
||||
created, err := f.Copy(ctx, path.Join(src, item.Name()), path.Join(dst, item.Name()), opts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_ = created
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
in, err := f.store.Open(src)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
out, err := f.store.Create(dst)
|
||||
if err != nil {
|
||||
in.Close()
|
||||
return false, err
|
||||
}
|
||||
_, copyErr := io.Copy(out, in)
|
||||
in.Close()
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
return false, copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return false, closeErr
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
||||
src := davStoragePath(name)
|
||||
dst := davStoragePath(dest)
|
||||
if opts != nil && opts.NoOverwrite {
|
||||
if _, err := f.store.Stat(dst); err == nil {
|
||||
return false, fmt.Errorf("destination exists")
|
||||
}
|
||||
}
|
||||
if m, ok := f.store.(storage.Multi); ok {
|
||||
srcRoot, _, srcOK := m.FindRoot(src)
|
||||
dstRoot, _, dstOK := m.FindRoot(dst)
|
||||
if srcOK && dstOK && srcRoot != dstRoot {
|
||||
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root move is not supported"))
|
||||
}
|
||||
}
|
||||
if err := f.store.Rename(src, dst); err != nil {
|
||||
return false, webdav.NewHTTPError(http.StatusForbidden, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type webdavMultiFS struct {
|
||||
roots map[string]webdav.FileSystem
|
||||
names []string
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"chfs-ng/internal/auth"
|
||||
"chfs-ng/internal/storage"
|
||||
"github.com/emersion/go-webdav"
|
||||
)
|
||||
|
||||
@@ -35,3 +36,22 @@ func TestWebDAVAuthFSCheckTree(t *testing.T) {
|
||||
t.Fatalf("public file should be deletable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVStorageFSCopyAndMove(t *testing.T) {
|
||||
root1 := t.TempDir()
|
||||
root2 := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root1, "a.txt"), []byte("one"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := storage.NewMulti([]string{root1, root2})
|
||||
fs := &webdavStorageFS{store: m}
|
||||
if _, err := fs.Copy(context.Background(), "/"+m.Names[0]+"/a.txt", "/"+m.Names[0]+"/b.txt", &webdav.CopyOptions{}); err != nil {
|
||||
t.Fatalf("copy: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root1, "b.txt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := fs.Move(context.Background(), "/"+m.Names[0]+"/b.txt", "/"+m.Names[1]+"/b.txt", &webdav.MoveOptions{}); err == nil {
|
||||
t.Fatal("expected cross-root move rejection")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,25 @@ func NewMulti(paths []string) Multi {
|
||||
|
||||
func (m Multi) isVirtualRoot(p string) bool { return strings.Trim(p, "/") == "" }
|
||||
|
||||
func (m Multi) FindRoot(p string) (string, string, bool) {
|
||||
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
|
||||
if p == "" {
|
||||
return "", "", false
|
||||
}
|
||||
parts := strings.SplitN(p, "/", 2)
|
||||
for _, n := range m.Names {
|
||||
if parts[0] != n {
|
||||
continue
|
||||
}
|
||||
rel := ""
|
||||
if len(parts) == 2 {
|
||||
rel = parts[1]
|
||||
}
|
||||
return n, rel, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func (m Multi) find(p string) (Local, string, bool) {
|
||||
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
|
||||
if p == "" {
|
||||
|
||||
Reference in New Issue
Block a user