mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-18 22:14:33 +00:00
webdav: describe a listed entry the way clients expect (#10993)
* webdav: name the entry, not its path, in a listing DAV:displayname carried the full path of every entry. A client that takes displayname for the child's name - Windows Explorer does - then looks for /dir/name under /dir and finds nothing, so a folder shows up empty while the root, where the two spellings differ only by a leading slash, still lists. Readdir now builds its entries with toFileInfo like stat does, so a listing and a lookup describe a child the same way, and the wrapper that was trimming the sub-folder back off a name goes away with it. Claude-Session: https://claude.ai/code/session_01XCeuCWpF9xo9CfyHvCQE9c * webdav: derive an ETag when nothing hashed the entry Uploads through this gateway carry no content MD5, so filer.ETag comes back empty and every file in a PROPFIND answered with an empty DAV:getetag, which is not a valid entity-tag. Report it as unimplemented instead, the way the sub-folder wrapper already did, and webdav falls back to modification time and size. The wrapper's copy went with it - it swallowed the stat error a caller was meant to see. Claude-Session: https://claude.ai/code/session_01XCeuCWpF9xo9CfyHvCQE9c
This commit is contained in:
@@ -115,6 +115,11 @@ func (fi *FileInfo) ETag(ctx context.Context) (string, error) {
|
||||
if fi.err != nil {
|
||||
return "", fi.err
|
||||
}
|
||||
if fi.etag == "" {
|
||||
// nothing hashed this entry; let webdav derive one rather than
|
||||
// publish an empty DAV:getetag, which is not a valid entity-tag
|
||||
return "", webdav.ErrNotImplemented
|
||||
}
|
||||
return fi.etag, nil
|
||||
}
|
||||
|
||||
@@ -382,11 +387,7 @@ func (fs *WebDavFileSystem) stat(ctx context.Context, fullFilePath string) (os.F
|
||||
fullpath := util.FullPath(fullFilePath)
|
||||
|
||||
if listedFi := listedEntriesFrom(ctx).get(string(fullpath)); listedFi != nil {
|
||||
// the caller's spelling of the path, trailing slash and all, is what a
|
||||
// lookup would have reported back
|
||||
fi := *listedFi
|
||||
fi.name = string(fullpath)
|
||||
return &fi, nil
|
||||
return listedFi, nil
|
||||
}
|
||||
|
||||
entry, _, _, err := filer_pb.GetEntry(ctx, fs, fullpath)
|
||||
@@ -404,16 +405,19 @@ func (fs *WebDavFileSystem) stat(ctx context.Context, fullFilePath string) (os.F
|
||||
}
|
||||
|
||||
func toFileInfo(fullpath util.FullPath, entry *filer_pb.Entry) *FileInfo {
|
||||
// Name is what WebDAV publishes as DAV:displayname; clients that build a
|
||||
// child URL from it reach nothing when it carries the whole path.
|
||||
fp := util.FullPath(listedEntryKey(string(fullpath)))
|
||||
fi := &FileInfo{
|
||||
size: int64(filer.FileSize(entry)),
|
||||
name: string(fullpath),
|
||||
name: fp.Name(),
|
||||
mode: os.FileMode(entry.Attributes.FileMode),
|
||||
modifiedTime: time.Unix(entry.Attributes.Mtime, 0),
|
||||
etag: filer.ETag(entry),
|
||||
isDirectory: entry.IsDirectory,
|
||||
}
|
||||
|
||||
if fi.name == "/" {
|
||||
if fp == "/" {
|
||||
fi.modifiedTime = time.Now()
|
||||
fi.isDirectory = true
|
||||
}
|
||||
@@ -594,24 +598,14 @@ func (f *WebDavFile) Readdir(count int) (ret []os.FileInfo, err error) {
|
||||
listed := listedEntriesFrom(ctx)
|
||||
|
||||
err = filer_pb.ReadDirAllEntries(ctx, f.fs, util.FullPath(dir), "", func(entry *filer_pb.Entry, isLast bool) error {
|
||||
fi := FileInfo{
|
||||
size: int64(filer.FileSize(entry)),
|
||||
name: entry.Name,
|
||||
mode: os.FileMode(entry.Attributes.FileMode),
|
||||
modifiedTime: time.Unix(entry.Attributes.Mtime, 0),
|
||||
isDirectory: entry.IsDirectory,
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(fi.name, "/") && fi.IsDir() {
|
||||
fi.name += "/"
|
||||
}
|
||||
childPath := util.NewFullPath(dir, entry.Name)
|
||||
fi := toFileInfo(childPath, entry)
|
||||
|
||||
glog.V(4).Infof("entry: %v", fi.name)
|
||||
|
||||
childPath := util.NewFullPath(dir, entry.Name)
|
||||
listed.put(string(childPath), toFileInfo(childPath, entry))
|
||||
listed.put(string(childPath), fi)
|
||||
|
||||
ret = append(ret, &fi)
|
||||
ret = append(ret, fi)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func TestToFileInfoName(t *testing.T) {
|
||||
tests := []struct {
|
||||
fullpath string
|
||||
want string
|
||||
}{
|
||||
{"/photo.jpg", "photo.jpg"},
|
||||
{"/Images/photo.jpg", "photo.jpg"},
|
||||
{"/Images/2026/photo.jpg", "photo.jpg"},
|
||||
{"/Images", "Images"},
|
||||
{"/Images/", "Images"},
|
||||
{"/", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
entry := &filer_pb.Entry{Name: "photo.jpg", Attributes: &filer_pb.FuseAttributes{}}
|
||||
fi := toFileInfo(util.FullPath(tt.fullpath), entry)
|
||||
if fi.Name() != tt.want {
|
||||
t.Errorf("toFileInfo(%q).Name() = %q, want %q (DAV:displayname must not carry the path)", tt.fullpath, fi.Name(), tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToFileInfoRootIsDirectory(t *testing.T) {
|
||||
entry := &filer_pb.Entry{Attributes: &filer_pb.FuseAttributes{}}
|
||||
if !toFileInfo("/", entry).IsDir() {
|
||||
t.Error("root is not a directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoETag(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := (&FileInfo{}).ETag(ctx); err != webdav.ErrNotImplemented {
|
||||
t.Errorf("empty etag returned %v, want ErrNotImplemented so webdav derives one", err)
|
||||
}
|
||||
if etag, err := (&FileInfo{etag: "abc"}).ETag(ctx); err != nil || etag != "abc" {
|
||||
t.Errorf("ETag() = %q, %v, want \"abc\", nil", etag, err)
|
||||
}
|
||||
|
||||
failed := &FileInfo{err: os.ErrInvalid}
|
||||
if _, err := failed.ETag(ctx); err != os.ErrInvalid {
|
||||
t.Errorf("ETag() = %v, want the stat error", err)
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ package weed_server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
@@ -23,10 +21,8 @@ func (w wrappedFs) confine(name string) string {
|
||||
return w.subFolder + path.Clean("/"+name)
|
||||
}
|
||||
|
||||
// NewWrappedFs returns a webdav.FileSystem identical to fs, except it
|
||||
// provides access to a sub-folder of fs that is denominated by subFolder.
|
||||
// It transparently handles renaming paths and filenames so that the outer part of the wrapped filesystem
|
||||
// does not leak out.
|
||||
// NewWrappedFs returns a webdav.FileSystem identical to fs, except that it
|
||||
// serves only the subFolder sub-tree of fs.
|
||||
func NewWrappedFs(fs webdav.FileSystem, subFolder string) webdav.FileSystem {
|
||||
return wrappedFs{
|
||||
subFolder: subFolder,
|
||||
@@ -41,13 +37,7 @@ func (w wrappedFs) Mkdir(ctx context.Context, name string, perm os.FileMode) err
|
||||
|
||||
func (w wrappedFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
name = w.confine(name)
|
||||
file, err := w.FileSystem.OpenFile(ctx, name, flag, perm)
|
||||
file = wrappedFile{
|
||||
File: file,
|
||||
subFolder: &w.subFolder,
|
||||
}
|
||||
|
||||
return file, err
|
||||
return w.FileSystem.OpenFile(ctx, name, flag, perm)
|
||||
}
|
||||
|
||||
func (w wrappedFs) RemoveAll(ctx context.Context, name string) error {
|
||||
@@ -63,53 +53,5 @@ func (w wrappedFs) Rename(ctx context.Context, oldName, newName string) error {
|
||||
|
||||
func (w wrappedFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
name = w.confine(name)
|
||||
info, err := w.FileSystem.Stat(ctx, name)
|
||||
info = wrappedFileInfo{
|
||||
subFolder: &w.subFolder,
|
||||
FileInfo: info,
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
|
||||
type wrappedFile struct {
|
||||
webdav.File
|
||||
subFolder *string
|
||||
}
|
||||
|
||||
func (w wrappedFile) Readdir(count int) ([]fs.FileInfo, error) {
|
||||
infos, err := w.File.Readdir(count)
|
||||
for i, info := range infos {
|
||||
infos[i] = wrappedFileInfo{
|
||||
subFolder: w.subFolder,
|
||||
FileInfo: info,
|
||||
}
|
||||
}
|
||||
return infos, err
|
||||
}
|
||||
|
||||
func (w wrappedFile) Stat() (fs.FileInfo, error) {
|
||||
info, err := w.File.Stat()
|
||||
info = wrappedFileInfo{
|
||||
subFolder: w.subFolder,
|
||||
FileInfo: info,
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
|
||||
type wrappedFileInfo struct {
|
||||
subFolder *string
|
||||
fs.FileInfo
|
||||
}
|
||||
|
||||
func (w wrappedFileInfo) Name() string {
|
||||
name := w.FileInfo.Name()
|
||||
return strings.TrimPrefix(name, *w.subFolder)
|
||||
}
|
||||
|
||||
func (w wrappedFileInfo) ETag(ctx context.Context) (string, error) {
|
||||
etag, _ := w.FileInfo.(webdav.ETager).ETag(ctx)
|
||||
if len(etag) == 0 {
|
||||
return etag, webdav.ErrNotImplemented
|
||||
}
|
||||
return etag, nil
|
||||
return w.FileSystem.Stat(ctx, name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user