From 5f15d1d8bbfea9387bf1107453213222ac3f52f7 Mon Sep 17 00:00:00 2001 From: Will Scott Date: Tue, 2 Jan 2024 16:38:21 +0100 Subject: [PATCH 1/5] go fmt for file/file_windows.go --- file/file_windows.go | 1 + 1 file changed, 1 insertion(+) diff --git a/file/file_windows.go b/file/file_windows.go index 1a41b22..cbd081a 100644 --- a/file/file_windows.go +++ b/file/file_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package file From f707f3ce95f92a67c56cd0cad2d8ad8becbae486 Mon Sep 17 00:00:00 2001 From: Will Scott Date: Tue, 2 Jan 2024 16:41:43 +0100 Subject: [PATCH 2/5] Invalidate handle on deletion as well as on rename --- nfs_onremove.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nfs_onremove.go b/nfs_onremove.go index c07f82d..222cc82 100644 --- a/nfs_onremove.go +++ b/nfs_onremove.go @@ -57,6 +57,10 @@ func onRemove(ctx context.Context, w *response, userHandle Handler) error { return &NFSStatusError{NFSStatusIO, err} } + if err := userHandle.InvalidateHandle(fs, obj.Handle); err != nil { + return &NFSStatusError{NFSStatusServerFault, err} + } + writer := bytes.NewBuffer([]byte{}) if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { return &NFSStatusError{NFSStatusServerFault, err} From f8b41770a60ee87dd0115a1436d0a4edf1f4181f Mon Sep 17 00:00:00 2001 From: Will Scott Date: Tue, 2 Jan 2024 16:43:25 +0100 Subject: [PATCH 3/5] finish move to preferred go:build formatting --- example/osnfs/changeos_unix.go | 1 - file/file_unix.go | 1 - file/file_windows.go | 1 - 3 files changed, 3 deletions(-) diff --git a/example/osnfs/changeos_unix.go b/example/osnfs/changeos_unix.go index 1c6d1ca..2fe93c6 100644 --- a/example/osnfs/changeos_unix.go +++ b/example/osnfs/changeos_unix.go @@ -1,5 +1,4 @@ //go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris package main diff --git a/file/file_unix.go b/file/file_unix.go index 5ee0589..6658c20 100644 --- a/file/file_unix.go +++ b/file/file_unix.go @@ -1,5 +1,4 @@ //go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris package file diff --git a/file/file_windows.go b/file/file_windows.go index cbd081a..ef173d5 100644 --- a/file/file_windows.go +++ b/file/file_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package file From 434a85611c044b2e3212560bb0c800e02c4b9e41 Mon Sep 17 00:00:00 2001 From: Will Scott Date: Thu, 4 Jan 2024 10:52:57 +0100 Subject: [PATCH 4/5] Update test to validate large directories and rename --- go.mod | 2 +- go.sum | 9 +- helpers/memfs/memfs.go | 414 +++++++++++++++++++++++++++++++++++++++ helpers/memfs/storage.go | 240 +++++++++++++++++++++++ nfs_onreaddirplus.go | 6 +- nfs_onrename.go | 4 +- nfs_test.go | 45 ++++- 7 files changed, 707 insertions(+), 13 deletions(-) create mode 100644 helpers/memfs/memfs.go create mode 100644 helpers/memfs/storage.go diff --git a/go.mod b/go.mod index 09d84c4..d10ae87 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/uuid v1.5.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 - github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 + github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 github.com/willscott/memphis v0.0.0-20210922141505-529d4987ab7e golang.org/x/sys v0.15.0 ) diff --git a/go.sum b/go.sum index 832a733..bff1fa1 100644 --- a/go.sum +++ b/go.sum @@ -40,11 +40,14 @@ github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e h1:FIB2fi7XJGHI github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e/go.mod h1:/qe02xr3jvTUz8u/PV0FHGpP8t96OQNP7U9BJMwMLEw= github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a h1:G++j5e0OC488te356JvdhaM8YS6nMsjLAYF7JxCv07w= github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= -github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 h1:Wd8wdpRzPXskyHvZLyw7Wc1fp5oCE2mhBCj7bAiibUs= -github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33/go.mod h1:cOUKSNty+RabZqKhm5yTJT5Vq/Fe83ZRWAJ5Kj8nRes= +github.com/willscott/go-nfs-client v0.0.0-20240103141042-dfd9c8c8f5a0 h1:O5rrM8FhTIvB2ZcYZmGH8NCRKP/lMrTbm0BmtctC+P8= +github.com/willscott/go-nfs-client v0.0.0-20240103141042-dfd9c8c8f5a0/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA= +github.com/willscott/go-nfs-client v0.0.0-20240103142856-58d3345adcd2 h1:OmuLQZHleuwzqCLLCQYQStOPirEjE35S04vPajNMQjI= +github.com/willscott/go-nfs-client v0.0.0-20240103142856-58d3345adcd2/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA= +github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o= +github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA= github.com/willscott/memphis v0.0.0-20210922141505-529d4987ab7e h1:1eHCP4w7tMmpfFBdrd5ff+vYU9THtrtA1yM9f0TLlJw= github.com/willscott/memphis v0.0.0-20210922141505-529d4987ab7e/go.mod h1:59vHBW4EpjiL5oiqgCrBp1Tc9JXRzKCNMEOaGmNfSHo= -github.com/zema1/go-nfs-client v0.0.0-20200604081958-0cf942f0e0fe/go.mod h1:im3CVJ32XM3+E+2RhY0sa5IVJVQehUrX0oE1wX4xOwU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= diff --git a/helpers/memfs/memfs.go b/helpers/memfs/memfs.go new file mode 100644 index 0000000..5e1822a --- /dev/null +++ b/helpers/memfs/memfs.go @@ -0,0 +1,414 @@ +// Package memfs is a variant of "github.com/go-git/go-billy/v5/memfs" with +// stable mtimes for items. +package memfs + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + "time" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/helper/chroot" + "github.com/go-git/go-billy/v5/util" +) + +const separator = filepath.Separator + +// Memory a very convenient filesystem based on memory files +type Memory struct { + s *storage +} + +// New returns a new Memory filesystem. +func New() billy.Filesystem { + fs := &Memory{s: newStorage()} + return chroot.New(fs, string(separator)) +} + +func (fs *Memory) Create(filename string) (billy.File, error) { + return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) +} + +func (fs *Memory) Open(filename string) (billy.File, error) { + return fs.OpenFile(filename, os.O_RDONLY, 0) +} + +func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { + f, has := fs.s.Get(filename) + if !has { + if !isCreate(flag) { + return nil, os.ErrNotExist + } + + var err error + f, err = fs.s.New(filename, perm, flag) + if err != nil { + return nil, err + } + } else { + if isExclusive(flag) { + return nil, os.ErrExist + } + + if target, isLink := fs.resolveLink(filename, f); isLink { + return fs.OpenFile(target, flag, perm) + } + } + + if f.mode.IsDir() { + return nil, fmt.Errorf("cannot open directory: %s", filename) + } + + return f.Duplicate(filename, perm, flag), nil +} + +func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) { + if !isSymlink(f.mode) { + return fullpath, false + } + + target = string(f.content.bytes) + if !isAbs(target) { + target = fs.Join(filepath.Dir(fullpath), target) + } + + return target, true +} + +// On Windows OS, IsAbs validates if a path is valid based on if stars with a +// unit (eg.: `C:\`) to assert that is absolute, but in this mem implementation +// any path starting by `separator` is also considered absolute. +func isAbs(path string) bool { + return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator)) +} + +func (fs *Memory) Stat(filename string) (os.FileInfo, error) { + f, has := fs.s.Get(filename) + if !has { + return nil, os.ErrNotExist + } + + fi, _ := f.Stat() + + var err error + if target, isLink := fs.resolveLink(filename, f); isLink { + fi, err = fs.Stat(target) + if err != nil { + return nil, err + } + } + + // the name of the file should always the name of the stated file, so we + // overwrite the Stat returned from the storage with it, since the + // filename may belong to a link. + fi.(*fileInfo).name = filepath.Base(filename) + return fi, nil +} + +func (fs *Memory) Lstat(filename string) (os.FileInfo, error) { + f, has := fs.s.Get(filename) + if !has { + return nil, os.ErrNotExist + } + + return f.Stat() +} + +type ByName []os.FileInfo + +func (a ByName) Len() int { return len(a) } +func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() } +func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) { + if f, has := fs.s.Get(path); has { + if target, isLink := fs.resolveLink(path, f); isLink { + return fs.ReadDir(target) + } + } else { + return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT} + } + + var entries []os.FileInfo + for _, f := range fs.s.Children(path) { + fi, _ := f.Stat() + entries = append(entries, fi) + } + + sort.Sort(ByName(entries)) + + return entries, nil +} + +func (fs *Memory) MkdirAll(path string, perm os.FileMode) error { + _, err := fs.s.New(path, perm|os.ModeDir, 0) + return err +} + +func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) { + return util.TempFile(fs, dir, prefix) +} + +func (fs *Memory) Rename(from, to string) error { + return fs.s.Rename(from, to) +} + +func (fs *Memory) Remove(filename string) error { + return fs.s.Remove(filename) +} + +func (fs *Memory) Join(elem ...string) string { + return filepath.Join(elem...) +} + +func (fs *Memory) Symlink(target, link string) error { + _, err := fs.Stat(link) + if err == nil { + return os.ErrExist + } + + if !os.IsNotExist(err) { + return err + } + + return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink) +} + +func (fs *Memory) Readlink(link string) (string, error) { + f, has := fs.s.Get(link) + if !has { + return "", os.ErrNotExist + } + + if !isSymlink(f.mode) { + return "", &os.PathError{ + Op: "readlink", + Path: link, + Err: fmt.Errorf("not a symlink"), + } + } + + return string(f.content.bytes), nil +} + +// Capabilities implements the Capable interface. +func (fs *Memory) Capabilities() billy.Capability { + return billy.WriteCapability | + billy.ReadCapability | + billy.ReadAndWriteCapability | + billy.SeekCapability | + billy.TruncateCapability +} + +type file struct { + name string + content *content + position int64 + flag int + mode os.FileMode + mtime time.Time + + isClosed bool +} + +func (f *file) Name() string { + return f.name +} + +func (f *file) Read(b []byte) (int, error) { + n, err := f.ReadAt(b, f.position) + f.position += int64(n) + + if err == io.EOF && n != 0 { + err = nil + } + + return n, err +} + +func (f *file) ReadAt(b []byte, off int64) (int, error) { + if f.isClosed { + return 0, os.ErrClosed + } + + if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) { + return 0, errors.New("read not supported") + } + + n, err := f.content.ReadAt(b, off) + + return n, err +} + +func (f *file) Seek(offset int64, whence int) (int64, error) { + if f.isClosed { + return 0, os.ErrClosed + } + + switch whence { + case io.SeekCurrent: + f.position += offset + case io.SeekStart: + f.position = offset + case io.SeekEnd: + f.position = int64(f.content.Len()) + offset + } + + return f.position, nil +} + +func (f *file) Write(p []byte) (int, error) { + return f.WriteAt(p, f.position) +} + +func (f *file) WriteAt(p []byte, off int64) (int, error) { + if f.isClosed { + return 0, os.ErrClosed + } + + if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) { + return 0, errors.New("write not supported") + } + + n, err := f.content.WriteAt(p, off) + f.position = off + int64(n) + f.mtime = time.Now() + + return n, err +} + +func (f *file) Close() error { + if f.isClosed { + return os.ErrClosed + } + + f.isClosed = true + return nil +} + +func (f *file) Truncate(size int64) error { + if size < int64(len(f.content.bytes)) { + f.content.bytes = f.content.bytes[:size] + } else if more := int(size) - len(f.content.bytes); more > 0 { + f.content.bytes = append(f.content.bytes, make([]byte, more)...) + } + f.mtime = time.Now() + + return nil +} + +func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File { + new := &file{ + name: filename, + content: f.content, + mode: mode, + flag: flag, + mtime: time.Now(), + } + + if isTruncate(flag) { + new.content.Truncate() + } + + if isAppend(flag) { + new.position = int64(new.content.Len()) + } + + return new +} + +func (f *file) Stat() (os.FileInfo, error) { + return &fileInfo{ + name: f.Name(), + mode: f.mode, + size: f.content.Len(), + mtime: f.mtime, + }, nil +} + +// Lock is a no-op in memfs. +func (f *file) Lock() error { + return nil +} + +// Unlock is a no-op in memfs. +func (f *file) Unlock() error { + return nil +} + +type fileInfo struct { + name string + size int + mode os.FileMode + mtime time.Time +} + +func (fi *fileInfo) Name() string { + return fi.name +} + +func (fi *fileInfo) Size() int64 { + return int64(fi.size) +} + +func (fi *fileInfo) Mode() os.FileMode { + return fi.mode +} + +func (fi *fileInfo) ModTime() time.Time { + return fi.mtime +} + +func (fi *fileInfo) IsDir() bool { + return fi.mode.IsDir() +} + +func (*fileInfo) Sys() interface{} { + return nil +} + +func (c *content) Truncate() { + c.bytes = make([]byte, 0) +} + +func (c *content) Len() int { + return len(c.bytes) +} + +func isCreate(flag int) bool { + return flag&os.O_CREATE != 0 +} + +func isExclusive(flag int) bool { + return flag&os.O_EXCL != 0 +} + +func isAppend(flag int) bool { + return flag&os.O_APPEND != 0 +} + +func isTruncate(flag int) bool { + return flag&os.O_TRUNC != 0 +} + +func isReadAndWrite(flag int) bool { + return flag&os.O_RDWR != 0 +} + +func isReadOnly(flag int) bool { + return flag == os.O_RDONLY +} + +func isWriteOnly(flag int) bool { + return flag&os.O_WRONLY != 0 +} + +func isSymlink(m os.FileMode) bool { + return m&os.ModeSymlink != 0 +} diff --git a/helpers/memfs/storage.go b/helpers/memfs/storage.go new file mode 100644 index 0000000..7597a03 --- /dev/null +++ b/helpers/memfs/storage.go @@ -0,0 +1,240 @@ +package memfs + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" +) + +type storage struct { + files map[string]*file + children map[string]map[string]*file +} + +func newStorage() *storage { + return &storage{ + files: make(map[string]*file, 0), + children: make(map[string]map[string]*file, 0), + } +} + +func (s *storage) Has(path string) bool { + path = clean(path) + + _, ok := s.files[path] + return ok +} + +func (s *storage) New(path string, mode os.FileMode, flag int) (*file, error) { + path = clean(path) + if s.Has(path) { + if !s.MustGet(path).mode.IsDir() { + return nil, fmt.Errorf("file already exists %q", path) + } + + return nil, nil + } + + name := filepath.Base(path) + + f := &file{ + name: name, + content: &content{name: name}, + mode: mode, + flag: flag, + mtime: time.Now(), + } + + s.files[path] = f + s.createParent(path, mode, f) + return f, nil +} + +func (s *storage) createParent(path string, mode os.FileMode, f *file) error { + base := filepath.Dir(path) + base = clean(base) + if f.Name() == string(separator) { + return nil + } + + if _, err := s.New(base, mode.Perm()|os.ModeDir, 0); err != nil { + return err + } + + if _, ok := s.children[base]; !ok { + s.children[base] = make(map[string]*file, 0) + } + + s.children[base][f.Name()] = f + return nil +} + +func (s *storage) Children(path string) []*file { + path = clean(path) + + l := make([]*file, 0) + for _, f := range s.children[path] { + l = append(l, f) + } + + return l +} + +func (s *storage) MustGet(path string) *file { + f, ok := s.Get(path) + if !ok { + panic(fmt.Errorf("couldn't find %q", path)) + } + + return f +} + +func (s *storage) Get(path string) (*file, bool) { + path = clean(path) + if !s.Has(path) { + return nil, false + } + + file, ok := s.files[path] + return file, ok +} + +func (s *storage) Rename(from, to string) error { + from = clean(from) + to = clean(to) + + if !s.Has(from) { + return os.ErrNotExist + } + + move := [][2]string{{from, to}} + + for pathFrom := range s.files { + if pathFrom == from || !filepath.HasPrefix(pathFrom, from) { + continue + } + + rel, _ := filepath.Rel(from, pathFrom) + pathTo := filepath.Join(to, rel) + + move = append(move, [2]string{pathFrom, pathTo}) + } + + for _, ops := range move { + from := ops[0] + to := ops[1] + + if err := s.move(from, to); err != nil { + return err + } + } + + return nil +} + +func (s *storage) move(from, to string) error { + s.files[to] = s.files[from] + s.files[to].name = filepath.Base(to) + s.children[to] = s.children[from] + + defer func() { + delete(s.children, from) + delete(s.files, from) + delete(s.children[filepath.Dir(from)], filepath.Base(from)) + }() + + return s.createParent(to, 0644, s.files[to]) +} + +func (s *storage) Remove(path string) error { + path = clean(path) + + f, has := s.Get(path) + if !has { + return os.ErrNotExist + } + + if f.mode.IsDir() && len(s.children[path]) != 0 { + return fmt.Errorf("dir: %s contains files", path) + } + + base, file := filepath.Split(path) + base = filepath.Clean(base) + + delete(s.children[base], file) + delete(s.files, path) + return nil +} + +func clean(path string) string { + return filepath.Clean(filepath.FromSlash(path)) +} + +type content struct { + name string + bytes []byte + + m sync.RWMutex +} + +func (c *content) WriteAt(p []byte, off int64) (int, error) { + if off < 0 { + return 0, &os.PathError{ + Op: "writeat", + Path: c.name, + Err: errors.New("negative offset"), + } + } + + c.m.Lock() + prev := len(c.bytes) + + diff := int(off) - prev + if diff > 0 { + c.bytes = append(c.bytes, make([]byte, diff)...) + } + + c.bytes = append(c.bytes[:off], p...) + if len(c.bytes) < prev { + c.bytes = c.bytes[:prev] + } + c.m.Unlock() + + return len(p), nil +} + +func (c *content) ReadAt(b []byte, off int64) (n int, err error) { + if off < 0 { + return 0, &os.PathError{ + Op: "readat", + Path: c.name, + Err: errors.New("negative offset"), + } + } + + c.m.RLock() + size := int64(len(c.bytes)) + if off >= size { + c.m.RUnlock() + return 0, io.EOF + } + + l := int64(len(b)) + if off+l > size { + l = size - off + } + + btr := c.bytes[off : off+l] + n = copy(b, btr) + + if len(btr) < len(b) { + err = io.EOF + } + c.m.RUnlock() + + return +} diff --git a/nfs_onreaddirplus.go b/nfs_onreaddirplus.go index a58c707..e6796de 100644 --- a/nfs_onreaddirplus.go +++ b/nfs_onreaddirplus.go @@ -79,17 +79,21 @@ func onReadDirPlus(ctx context.Context, w *response, userHandle Handler) error { dotFileID = da.Fileid } entities = append(entities, - readDirPlusEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID}, + readDirPlusEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID, Attributes: da}, readDirPlusEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID}, ) } eof := true maxEntities := userHandle.HandleLimit() / 2 + fb := 0 + fss := 0 for i, c := range contents { // cookie equates to index within contents + 2 (for '.' and '..') cookie := uint64(i + 2) + fb++ if started { + fss++ dirBytes += uint32(len(c.Name()) + 20) maxBytes += 512 // TODO: better estimation. if dirBytes > obj.DirCount || maxBytes > obj.MaxCount || len(entities) > maxEntities { diff --git a/nfs_onrename.go b/nfs_onrename.go index b04b807..2cba4fe 100644 --- a/nfs_onrename.go +++ b/nfs_onrename.go @@ -71,6 +71,8 @@ func onRename(ctx context.Context, w *response, userHandle Handler) error { } preDestData := ToFileAttribute(toDirInfo, toDirPath).AsCache() + oldHandle := userHandle.ToHandle(fs, append(fromPath, string(from.Filename))) + fromLoc := fs.Join(append(fromPath, string(from.Filename))...) toLoc := fs.Join(append(toPath, string(to.Filename))...) @@ -85,7 +87,7 @@ func onRename(ctx context.Context, w *response, userHandle Handler) error { return &NFSStatusError{NFSStatusIO, err} } - if err := userHandle.InvalidateHandle(fs, from.Handle); err != nil { + if err := userHandle.InvalidateHandle(fs, oldHandle); err != nil { return &NFSStatusError{NFSStatusServerFault, err} } diff --git a/nfs_test.go b/nfs_test.go index 88d2745..00177ca 100644 --- a/nfs_test.go +++ b/nfs_test.go @@ -10,14 +10,19 @@ import ( nfs "github.com/willscott/go-nfs" "github.com/willscott/go-nfs/helpers" + "github.com/willscott/go-nfs/helpers/memfs" - "github.com/go-git/go-billy/v5/memfs" nfsc "github.com/willscott/go-nfs-client/nfs" rpc "github.com/willscott/go-nfs-client/nfs/rpc" + "github.com/willscott/go-nfs-client/nfs/util" "github.com/willscott/go-nfs-client/nfs/xdr" ) func TestNFS(t *testing.T) { + if testing.Verbose() { + util.DefaultLogger.SetDebug(true) + } + // make an empty in-memory server. listener, err := net.Listen("tcp", "localhost:0") if err != nil { @@ -34,7 +39,7 @@ func TestNFS(t *testing.T) { _ = nfs.Serve(listener, cacheHelper) }() - c, err := rpc.DialTCP(listener.Addr().Network(), nil, listener.Addr().(*net.TCPAddr).String()) + c, err := rpc.DialTCP(listener.Addr().Network(), listener.Addr().(*net.TCPAddr).String(), false) if err != nil { t.Fatal(err) } @@ -92,12 +97,12 @@ func TestNFS(t *testing.T) { if err != nil { t.Fatal(err) } - shouldBeNames := []string{".", ".."} + shouldBeNames := []string{} for _, f := range dirF1 { shouldBeNames = append(shouldBeNames, f.Name()) } - for i := 0; i < 100; i++ { - fName := fmt.Sprintf("f-%03d.txt", i) + for i := 0; i < 2000; i++ { + fName := fmt.Sprintf("f-%04d.txt", i) shouldBeNames = append(shouldBeNames, fName) f, err := mem.Create(fName) if err != nil { @@ -138,9 +143,32 @@ func TestNFS(t *testing.T) { as2.Sort() bs2.Sort() if !reflect.DeepEqual(as2, bs2) { + fmt.Printf("should be %v\n", as2) + fmt.Printf("actual be %v\n", bs2) t.Fatal("nfs.ReadDir error") } + // confirm rename works as expected + oldFA, _, err := target.Lookup("/f-0010.txt", false) + if err != nil { + t.Fatal(err) + } + + if err := target.Rename("/f-0010.txt", "/g-0010.txt"); err != nil { + t.Fatal(err) + } + new, _, err := target.Lookup("/g-0010.txt", false) + if err != nil { + t.Fatal(err) + } + if new.Sys() != oldFA.Sys() { + t.Fatal("rename failed to update") + } + _, _, err = target.Lookup("/f-0010.txt", false) + if err == nil { + t.Fatal("old handle should be invalid") + } + // for test nfs.ReadDirPlus in case of empty directory _, err = target.Mkdir("/empty", 0755) if err != nil { @@ -151,7 +179,7 @@ func TestNFS(t *testing.T) { if err != nil { t.Fatal(err) } - if len(emptyEntitiesPlus) != 2 || emptyEntitiesPlus[0].Name() != "." || emptyEntitiesPlus[1].Name() != ".." { + if len(emptyEntitiesPlus) != 0 { t.Fatal("nfs.ReadDirPlus error reading empty dir") } @@ -160,7 +188,7 @@ func TestNFS(t *testing.T) { if err != nil { t.Fatal(err) } - if len(emptyEntities) != 2 || emptyEntities[0].FileName != "." || emptyEntities[1].FileName != ".." { + if len(emptyEntities) != 0 { t.Fatal("nfs.ReadDir error reading empty dir") } } @@ -245,6 +273,9 @@ func readDir(target *nfsc.Target, dir string) ([]*readDirEntry, error) { } cookie = item.Entry.Cookie + if item.Entry.FileName == "." || item.Entry.FileName == ".." { + continue + } entries = append(entries, &item.Entry) } From 891f4cd3606437d213c9f80cabb5c2579ee2cdc4 Mon Sep 17 00:00:00 2001 From: Will Scott Date: Thu, 4 Jan 2024 10:56:25 +0100 Subject: [PATCH 5/5] fix lint warnings --- helpers/memfs/storage.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/helpers/memfs/storage.go b/helpers/memfs/storage.go index 7597a03..5d73331 100644 --- a/helpers/memfs/storage.go +++ b/helpers/memfs/storage.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "strings" "sync" "time" ) @@ -50,7 +51,9 @@ func (s *storage) New(path string, mode os.FileMode, flag int) (*file, error) { } s.files[path] = f - s.createParent(path, mode, f) + if err := s.createParent(path, mode, f); err != nil { + return nil, err + } return f, nil } @@ -114,7 +117,7 @@ func (s *storage) Rename(from, to string) error { move := [][2]string{{from, to}} for pathFrom := range s.files { - if pathFrom == from || !filepath.HasPrefix(pathFrom, from) { + if pathFrom == from || !strings.HasPrefix(pathFrom, from) { continue }