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
|
r.URL.Path = "/" + vp
|
||||||
defer func() { r.URL.Path = old }()
|
defer func() { r.URL.Path = old }()
|
||||||
inner := s.WebDAVFS
|
inner := s.WebDAVFS
|
||||||
if inner == nil && s.MultiRoot {
|
|
||||||
if m, ok := s.Store.(storage.Multi); ok {
|
|
||||||
inner = newWebDAVMultiFS(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if inner == nil {
|
if inner == nil {
|
||||||
inner = webdav.LocalFileSystem(s.WebDAVRoot)
|
inner = &webdavStorageFS{store: s.Store, removeMode: s.RemoveMode}
|
||||||
}
|
}
|
||||||
fs := &webdavAuthFS{inner: inner, account: account(r, s)}
|
fs := &webdavAuthFS{inner: inner, account: account(r, s)}
|
||||||
(&webdav.Handler{FileSystem: fs}).ServeHTTP(w, r)
|
(&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 {
|
type webdavMultiFS struct {
|
||||||
roots map[string]webdav.FileSystem
|
roots map[string]webdav.FileSystem
|
||||||
names []string
|
names []string
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"chfs-ng/internal/auth"
|
"chfs-ng/internal/auth"
|
||||||
|
"chfs-ng/internal/storage"
|
||||||
"github.com/emersion/go-webdav"
|
"github.com/emersion/go-webdav"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,3 +36,22 @@ func TestWebDAVAuthFSCheckTree(t *testing.T) {
|
|||||||
t.Fatalf("public file should be deletable: %v", err)
|
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) 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) {
|
func (m Multi) find(p string) (Local, string, bool) {
|
||||||
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
|
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
|
||||||
if p == "" {
|
if p == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user