diff --git a/weed/server/webdav_server.go b/weed/server/webdav_server.go index f4023c50f..31d138508 100644 --- a/weed/server/webdav_server.go +++ b/weed/server/webdav_server.go @@ -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 { diff --git a/weed/server/webdav_server_test.go b/weed/server/webdav_server_test.go new file mode 100644 index 000000000..3e82d70c5 --- /dev/null +++ b/weed/server/webdav_server_test.go @@ -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) + } +} diff --git a/weed/server/wrapped_webdav_fs.go b/weed/server/wrapped_webdav_fs.go index 86de33e0f..a2fa1c2bc 100644 --- a/weed/server/wrapped_webdav_fs.go +++ b/weed/server/wrapped_webdav_fs.go @@ -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) }