From 7f9210759e02804d9edb48267a918d3cc7289b2e Mon Sep 17 00:00:00 2001 From: Zero <54469706+3JoB@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:58:33 +0800 Subject: [PATCH] add --- .vscode/settings.json | 8 + README.md | 20 +-- dummy.go | 30 ++-- dummy_test.go | 3 + example_dummy_test.go | 7 +- example_os_test.go | 3 +- example_readonly_test.go | 3 +- example_test.go | 11 +- example_wrapping_test.go | 7 +- filesystem.go | 19 ++- filesystem_test.go | 4 +- go.mod | 3 + go.sum | 0 ioutil_test.go | 4 +- memfs/buffer.go | 18 ++- memfs/buffer_test.go | 4 +- memfs/example_test.go | 2 +- memfs/memfile.go | 8 +- memfs/memfile_test.go | 3 +- memfs/memfs.go | 136 ++++++++++------ memfs/memfs_test.go | 143 +++++++++++++++-- mountfs/example_test.go | 6 +- mountfs/mountfs.go | 21 ++- mountfs/mountfs_test.go | 36 +++-- os.go | 19 +++ os_test.go | 11 +- path.go | 4 +- path_test.go | 1 - prefixfs/prefixfs.go | 9 +- prefixfs/prefixfs_test.go | 62 ++++---- readonly.go | 23 ++- readonly_test.go | 10 +- walk.go | 80 ++++++++++ walk_test.go | 320 ++++++++++++++++++++++++++++++++++++++ 34 files changed, 856 insertions(+), 182 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 walk.go create mode 100644 walk_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8ccab99 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.words": [ + "newname", + "newpath", + "oldname", + "oldpath" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index b551a88..f484e3d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -vfs for golang [![Build Status](https://travis-ci.org/blang/vfs.svg?branch=master)](https://travis-ci.org/blang/vfs) [![GoDoc](https://godoc.org/github.com/blang/vfs?status.png)](https://godoc.org/github.com/blang/vfs) [![Coverage Status](https://img.shields.io/coveralls/blang/vfs.svg)](https://coveralls.io/r/blang/vfs?branch=master) [![Join the chat at https://gitter.im/blang/vfs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/blang/vfs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +vfs for golang [![Build Status](https://travis-ci.org/blang/vfs.svg?branch=master)](https://travis-ci.org/blang/vfs) [![GoDoc](https://godoc.org/github.com/3JoB/vfs?status.png)](https://godoc.org/github.com/3JoB/vfs) [![Coverage Status](https://img.shields.io/coveralls/blang/vfs.svg)](https://coveralls.io/r/blang/vfs?branch=master) [![Join the chat at https://gitter.im/blang/vfs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/blang/vfs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ====== vfs is library to support virtual filesystems. It provides basic abstractions of filesystems and implementations, like `OS` accessing the file system of the underlying OS and `memfs` a full filesystem in-memory. @@ -6,12 +6,12 @@ vfs is library to support virtual filesystems. It provides basic abstractions of Usage ----- ```bash -$ go get github.com/blang/vfs +$ go get github.com/3JoB/vfs ``` Note: Always vendor your dependencies or fix on a specific version tag. ```go -import github.com/blang/vfs +import github.com/3JoB/vfs ``` ```go @@ -52,7 +52,7 @@ fs.Mkdir("/memfs/testdir", 0777) fs.Mkdir("/tmp/testdir", 0777) ``` -Check detailed examples below. Also check the [GoDocs](http://godoc.org/github.com/blang/vfs). +Check detailed examples below. Also check the [GoDocs](http://godoc.org/github.com/3JoB/vfs). Why should I use this lib? ----- @@ -62,16 +62,16 @@ Why should I use this lib? - Easy to create your own filesystem - Mock a full filesystem for testing (or use included `memfs`) - Compose/Wrap Filesystems `ReadOnly(OS())` and write simple Wrappers -- Many features, see [GoDocs](http://godoc.org/github.com/blang/vfs) and examples below +- Many features, see [GoDocs](http://godoc.org/github.com/3JoB/vfs) and examples below Features and Examples ----- -- [OS Filesystem support](http://godoc.org/github.com/blang/vfs#example-OsFS) -- [ReadOnly Wrapper](http://godoc.org/github.com/blang/vfs#example-RoFS) -- [DummyFS for quick mocking](http://godoc.org/github.com/blang/vfs#example-DummyFS) -- [MemFS - full in-memory filesystem](http://godoc.org/github.com/blang/vfs/memfs#example-MemFS) -- [MountFS - support mounts across filesystems](http://godoc.org/github.com/blang/vfs/mountfs#example-MountFS) +- [OS Filesystem support](http://godoc.org/github.com/3JoB/vfs#example-OsFS) +- [ReadOnly Wrapper](http://godoc.org/github.com/3JoB/vfs#example-RoFS) +- [DummyFS for quick mocking](http://godoc.org/github.com/3JoB/vfs#example-DummyFS) +- [MemFS - full in-memory filesystem](http://godoc.org/github.com/3JoB/vfs/memfs#example-MemFS) +- [MountFS - support mounts across filesystems](http://godoc.org/github.com/3JoB/vfs/mountfs#example-MountFS) Current state: ALPHA ----- diff --git a/dummy.go b/dummy.go index a0359cd..674eee8 100644 --- a/dummy.go +++ b/dummy.go @@ -7,7 +7,7 @@ import ( // Dummy creates a new dummy filesystem which returns the given error on every operation. func Dummy(err error) *DummyFS { - return &DummyFS{err} + return &DummyFS{err: err} } // DummyFS is dummy filesystem which returns an error on every operation. @@ -21,6 +21,11 @@ func (fs DummyFS) PathSeparator() uint8 { return '/' } +// Open returns dummy error +func (fs DummyFS) Open(name string) (File, error) { + return fs.OpenFile(name, os.O_RDONLY, 0) +} + // OpenFile returns dummy error func (fs DummyFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { return nil, fs.err @@ -41,6 +46,11 @@ func (fs DummyFS) Mkdir(name string, perm os.FileMode) error { return fs.err } +// Symlink returns dummy error +func (fs DummyFS) Symlink(oldname, newname string) error { + return fs.err +} + // Stat returns dummy error func (fs DummyFS) Stat(name string) (os.FileInfo, error) { return nil, fs.err @@ -60,15 +70,15 @@ func (fs DummyFS) ReadDir(path string) ([]os.FileInfo, error) { // To create a DummyFS returning a dummyFile instead of an error // you can your own DummyFS: // -// type writeDummyFS struct { -// Filesystem -// } +// type writeDummyFS struct { +// Filesystem +// } // -// func (fs writeDummyFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { -// return DummyFile(dummyError), nil -// } +// func (fs writeDummyFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { +// return DummyFile(dummyError), nil +// } func DummyFile(err error) *DumFile { - return &DumFile{err} + return &DumFile{err: err} } // DumFile represents a dummy File @@ -124,7 +134,7 @@ type DumFileInfo struct { IMode os.FileMode IModTime time.Time IDir bool - ISys interface{} + ISys any } // Name returns the field IName @@ -153,6 +163,6 @@ func (fi DumFileInfo) IsDir() bool { } // Sys returns the field ISys -func (fi DumFileInfo) Sys() interface{} { +func (fi DumFileInfo) Sys() any { return fi.ISys } diff --git a/dummy_test.go b/dummy_test.go index 71af207..6b9ba17 100644 --- a/dummy_test.go +++ b/dummy_test.go @@ -13,6 +13,9 @@ func TestInterface(t *testing.T) { func TestDummyFS(t *testing.T) { fs := Dummy(errDum) + if _, err := fs.Open("/tmp/test123"); err != errDum { + t.Errorf("Open DummyError expected: %s", err) + } if _, err := fs.OpenFile("test", 0, 0); err != errDum { t.Errorf("OpenFile DummyError expected: %s", err) } diff --git a/example_dummy_test.go b/example_dummy_test.go index 56a41c7..48bf27a 100644 --- a/example_dummy_test.go +++ b/example_dummy_test.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) type myFS struct { @@ -14,7 +14,7 @@ type myFS struct { func MyFS() *myFS { return &myFS{ - vfs.Dummy(errors.New("Not implemented yet!")), + Filesystem: vfs.Dummy(errors.New("Not implemented yet!")), } } @@ -35,7 +35,6 @@ func ExampleDummyFS() { // and return the dummys error _, err := vfs.Create(fs, "/tmp/vfs/example.txt") if err != nil { - fmt.Printf("Error will be: Not implemented yet!\n") + fmt.Print("Error will be: Not implemented yet!\n") } - } diff --git a/example_os_test.go b/example_os_test.go index 420224f..763a2ac 100644 --- a/example_os_test.go +++ b/example_os_test.go @@ -3,11 +3,10 @@ package vfs_test import ( "fmt" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) func ExampleOsFS() { - // Create a vfs accessing the filesystem of the underlying OS osFS := vfs.OS() err := osFS.Mkdir("/tmp/vfs_example", 0777) diff --git a/example_readonly_test.go b/example_readonly_test.go index 1860b3f..7159032 100644 --- a/example_readonly_test.go +++ b/example_readonly_test.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) // Every vfs.Filesystem could be easily wrapped @@ -26,7 +26,6 @@ func ExampleRoFS() { if err != nil { fmt.Printf("Could not create file: %s\n", err) return - } defer f.Close() diff --git a/example_test.go b/example_test.go index 394bd91..ea9ccfb 100644 --- a/example_test.go +++ b/example_test.go @@ -1,11 +1,12 @@ package vfs_test import ( - "fmt" - "github.com/blang/vfs" - "github.com/blang/vfs/memfs" - "github.com/blang/vfs/mountfs" + "errors" "os" + + "github.com/3JoB/vfs" + "github.com/3JoB/vfs/memfs" + "github.com/3JoB/vfs/mountfs" ) func Example() { @@ -23,7 +24,7 @@ func Example() { // Return vfs.ErrReadOnly _, err := f.Write([]byte("Write on readonly fs?")) if err != nil { - fmt.Errorf("Filesystem is read only!\n") + panic(errors.New("filesystem is read only!\n")) } // Create a fully writable filesystem in memory diff --git a/example_wrapping_test.go b/example_wrapping_test.go index ea5cda1..e503a18 100644 --- a/example_wrapping_test.go +++ b/example_wrapping_test.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) type noNewDirs struct { @@ -13,7 +13,7 @@ type noNewDirs struct { } func NoNewDirs(fs vfs.Filesystem) *noNewDirs { - return &noNewDirs{fs} + return &noNewDirs{Filesystem: fs} } // Mkdir is disabled @@ -22,12 +22,11 @@ func (fs *noNewDirs) Mkdir(name string, perm os.FileMode) error { } func ExampleOsFS_myWrapper() { - // Disable Mkdirs on the OS Filesystem var fs vfs.Filesystem = NoNewDirs(vfs.OS()) err := fs.Mkdir("/tmp", 0777) if err != nil { - fmt.Printf("Mkdir disabled!\n") + fmt.Print("Mkdir disabled!\n") } } diff --git a/filesystem.go b/filesystem.go index 7271206..c3f2f9d 100644 --- a/filesystem.go +++ b/filesystem.go @@ -9,9 +9,10 @@ import ( var ( // ErrIsDirectory is returned if a file is a directory - ErrIsDirectory = errors.New("Is directory") + ErrIsDirectory = errors.New("is directory") + // ErrNotDirectory is returned if a file is not a directory - ErrNotDirectory = errors.New("Is not a directory") + ErrNotDirectory = errors.New("is not a directory") ) // Filesystem represents an abstract filesystem @@ -19,26 +20,34 @@ type Filesystem interface { PathSeparator() uint8 OpenFile(name string, flag int, perm os.FileMode) (File, error) Remove(name string) error + // RemoveAll(path string) error Rename(oldpath, newpath string) error + Mkdir(name string, perm os.FileMode) error - // Symlink(oldname, newname string) error + + Symlink(oldname, newname string) error + // TempDir() string // Chmod(name string, mode FileMode) error // Chown(name string, uid, gid int) error Stat(name string) (os.FileInfo, error) + Lstat(name string) (os.FileInfo, error) ReadDir(path string) ([]os.FileInfo, error) } // File represents a File with common operations. // It differs from os.File so e.g. Stat() needs to be called from the Filesystem instead. -// osfile.Stat() -> filesystem.Stat(file.Name()) +// +// osfile.Stat() -> filesystem.Stat(file.Name()) type File interface { Name() string Sync() error + // Truncate shrinks or extends the size of the File to the specified size. Truncate(int64) error + io.Reader io.ReaderAt io.Writer @@ -74,7 +83,7 @@ func MkdirAll(fs Filesystem, path string, perm os.FileMode) error { if dir.IsDir() { return nil } - return &os.PathError{"mkdir", path, ErrNotDirectory} + return &os.PathError{Op: "mkdir", Path: path, Err: ErrNotDirectory} } parts := SplitPath(path, string(fs.PathSeparator())) diff --git a/filesystem_test.go b/filesystem_test.go index 182c0ef..28253ab 100644 --- a/filesystem_test.go +++ b/filesystem_test.go @@ -178,7 +178,6 @@ func (fs *rmFS) ReadDir(path string) ([]os.FileInfo, error) { if fi.IsDir() { s := make([]os.FileInfo, len(fi.subfiles)) for i, sf := range fi.subfiles { - s[i] = sf } for _, sf := range s { @@ -219,7 +218,7 @@ func (fs *rmFS) Remove(name string) error { return nil } - return &os.PathError{"remove", name, os.ErrNotExist} + return &os.PathError{Op: "remove", Path: name, Err: os.ErrNotExist} } func TestRemoveAll(t *testing.T) { @@ -277,5 +276,4 @@ func TestRemoveAll(t *testing.T) { if _, ok := fs.files["/"]; !ok { t.Errorf("/ was removed") } - } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4f30fd1 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/3JoB/vfs + +go 1.20 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/ioutil_test.go b/ioutil_test.go index 3047194..b8680ab 100644 --- a/ioutil_test.go +++ b/ioutil_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/blang/vfs" - "github.com/blang/vfs/memfs" + "github.com/3JoB/vfs" + "github.com/3JoB/vfs/memfs" ) var ( diff --git a/memfs/buffer.go b/memfs/buffer.go index a0a78c5..4f36e98 100644 --- a/memfs/buffer.go +++ b/memfs/buffer.go @@ -3,7 +3,6 @@ package memfs import ( "errors" "io" - "os" ) // Buffer is a usable block of data similar to a file @@ -13,6 +12,7 @@ type Buffer interface { io.Writer io.Seeker io.Closer + // Truncate shrinks or extends the size of the Buffer to the specified size. Truncate(int64) error } @@ -21,7 +21,7 @@ type Buffer interface { const MinBufferSize = 512 // ErrTooLarge is thrown if it was not possible to enough memory -var ErrTooLarge = errors.New("Volume too large") +var ErrTooLarge = errors.New("volume too large") // Buf is a Buffer working on a slice of bytes. type Buf struct { @@ -38,18 +38,20 @@ func NewBuffer(buf *[]byte) *Buf { // Seek sets the offset for the next Read or Write on the buffer to offset, // interpreted according to whence: -// 0 (os.SEEK_SET) means relative to the origin of the file -// 1 (os.SEEK_CUR) means relative to the current offset -// 2 (os.SEEK_END) means relative to the end of the file +// +// 0 (os.SEEK_SET) means relative to the origin of the file +// 1 (os.SEEK_CUR) means relative to the current offset +// 2 (os.SEEK_END) means relative to the end of the file +// // It returns the new offset and an error, if any. func (v *Buf) Seek(offset int64, whence int) (int64, error) { var abs int64 switch whence { - case os.SEEK_SET: // Relative to the origin of the file + case io.SeekStart: // Relative to the origin of the file abs = offset - case os.SEEK_CUR: // Relative to the current offset + case io.SeekCurrent: // Relative to the current offset abs = int64(v.ptr) + offset - case os.SEEK_END: // Relative to the end + case io.SeekEnd: // Relative to the end abs = int64(len(*v.buf)) + offset default: return 0, errors.New("Seek: invalid whence") diff --git a/memfs/buffer_test.go b/memfs/buffer_test.go index 77324a1..f37d331 100644 --- a/memfs/buffer_test.go +++ b/memfs/buffer_test.go @@ -1,11 +1,10 @@ package memfs import ( + "io" "os" "reflect" "strings" - - "io" "testing" ) @@ -146,7 +145,6 @@ func TestVolumesConcurrentAccess(t *testing.T) { if n, err := v1.Read(p); err != nil || n != len(abc) || string(p) != abc { t.Errorf("Unexpected read error: %d %s, res: %s", n, err, string(p)) } - } func TestSeek(t *testing.T) { diff --git a/memfs/example_test.go b/memfs/example_test.go index b98ecec..ecf5bac 100644 --- a/memfs/example_test.go +++ b/memfs/example_test.go @@ -1,7 +1,7 @@ package memfs_test import ( - "github.com/blang/vfs/memfs" + "github.com/3JoB/vfs/memfs" ) func ExampleMemFS() { diff --git a/memfs/memfile.go b/memfs/memfile.go index a44d27c..3b68a2d 100644 --- a/memfs/memfile.go +++ b/memfs/memfile.go @@ -77,9 +77,11 @@ func (b *MemFile) Write(p []byte) (n int, err error) { // Seek sets the offset for the next Read or Write on the buffer to offset, // interpreted according to whence: -// 0 (os.SEEK_SET) means relative to the origin of the file -// 1 (os.SEEK_CUR) means relative to the current offset -// 2 (os.SEEK_END) means relative to the end of the file +// +// 0 (os.SEEK_SET) means relative to the origin of the file +// 1 (os.SEEK_CUR) means relative to the current offset +// 2 (os.SEEK_END) means relative to the end of the file +// // It returns the new offset and an error, if any. func (b *MemFile) Seek(offset int64, whence int) (n int64, err error) { b.mutex.RLock() diff --git a/memfs/memfile_test.go b/memfs/memfile_test.go index 1d93cb1..61acaeb 100644 --- a/memfs/memfile_test.go +++ b/memfs/memfile_test.go @@ -1,8 +1,9 @@ package memfs import ( - "github.com/blang/vfs" "testing" + + "github.com/3JoB/vfs" ) func TestFileInterface(t *testing.T) { diff --git a/memfs/memfs.go b/memfs/memfs.go index 6be44bd..a3a0d17 100644 --- a/memfs/memfs.go +++ b/memfs/memfs.go @@ -9,14 +9,16 @@ import ( "sync" "time" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) var ( // ErrReadOnly is returned if the file is read-only and write operations are disabled. ErrReadOnly = errors.New("File is read-only") + // ErrWriteOnly is returned if the file is write-only and read operations are disabled. ErrWriteOnly = errors.New("File is write-only") + // ErrIsDirectory is returned if the file under operation is not a regular file but a directory. ErrIsDirectory = errors.New("Is directory") ) @@ -31,6 +33,8 @@ type MemFS struct { lock *sync.RWMutex } +var _ vfs.Filesystem = &MemFS{} + // Create a new MemFS filesystem which entirely resides in memory func Create() *MemFS { root := &fileInfo{ @@ -57,7 +61,7 @@ type fileInfo struct { mutex *sync.RWMutex } -func (fi fileInfo) Sys() interface{} { +func (fi fileInfo) Sys() any { return fi.fs } @@ -77,9 +81,9 @@ func (fi fileInfo) IsDir() bool { // ModTime returns the modification time. // Modification time is updated on: -// - Creation -// - Rename -// - Open (except with O_RDONLY) +// - Creation +// - Rename +// - Open (except with O_RDONLY) func (fi fileInfo) ModTime() time.Time { return fi.modTime } @@ -112,10 +116,10 @@ func (fs *MemFS) Mkdir(name string, perm os.FileMode) error { base := filepath.Base(name) parent, fi, err := fs.fileInfo(name) if err != nil { - return &os.PathError{"mkdir", name, err} + return &os.PathError{Op: "mkdir", Path: name, Err: err} } if fi != nil { - return &os.PathError{"mkdir", name, fmt.Errorf("Directory %q already exists", name)} + return &os.PathError{Op: "mkdir", Path: name, Err: fmt.Errorf("directory %q already exists", name)} } fi = &fileInfo{ @@ -130,6 +134,18 @@ func (fs *MemFS) Mkdir(name string, perm os.FileMode) error { return nil } +func (fs *MemFS) Symlink(oldname, newname string) error { + file, err := fs.OpenFile( + newname, + os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + 0777|os.ModeSymlink) + if err != nil { + return err + } + file.Write([]byte(oldname)) + return nil +} + // byName implements sort.Interface type byName []os.FileInfo @@ -150,10 +166,10 @@ func (fs *MemFS) ReadDir(path string) ([]os.FileInfo, error) { path = filepath.Clean(path) _, fi, err := fs.fileInfo(path) if err != nil { - return nil, &os.PathError{"readdir", path, err} + return nil, &os.PathError{Op: "readdir", Path: path, Err: err} } if fi == nil || !fi.dir { - return nil, &os.PathError{"readdir", path, vfs.ErrNotDirectory} + return nil, &os.PathError{Op: "readdir", Path: path, Err: vfs.ErrNotDirectory} } fis := make([]os.FileInfo, 0, len(fi.childs)) @@ -165,43 +181,48 @@ func (fs *MemFS) ReadDir(path string) ([]os.FileInfo, error) { } func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err error) { - path = filepath.Clean(path) - segments := vfs.SplitPath(path, PathSeparator) + return fs.relativeFileInfo(fs.wd, path) +} +func (fs *MemFS) relativeFileInfo(wd *fileInfo, path string) (parent *fileInfo, node *fileInfo, err error) { + parent, segments := fs.dirSegments(wd, path) // Shortcut for working directory and root - if len(segments) == 1 { - if segments[0] == "" { - return nil, fs.root, nil - } else if segments[0] == "." { - return fs.wd.parent, fs.wd, nil - } + if len(segments) == 0 { + return parent.parent, parent, nil } // Determine root to traverse - parent = fs.root - if segments[0] == "." { - parent = fs.wd - } - segments = segments[1:] - - // Further directories - if len(segments) > 1 { - for _, seg := range segments[:len(segments)-1] { - - if parent.childs == nil { - return nil, nil, os.ErrNotExist + for _, seg := range segments[:len(segments)-1] { + if parent.childs == nil { + return nil, nil, os.ErrNotExist + } + entry, ok := parent.childs[seg] + if !ok { + return nil, nil, os.ErrNotExist + } + if entry.dir { + parent = entry + } else if entry.mode&os.ModeSymlink != 0 { + // Look up interior symlink + _, parent, err = fs.relativeFileInfo(parent, string(*entry.buf)) + if err != nil { + return nil, nil, err } - if entry, ok := parent.childs[seg]; ok && entry.dir { - parent = entry - } else { - return nil, nil, os.ErrNotExist + // Symlink was not to a directory + if parent == nil { + return nil, nil, vfs.ErrNotDirectory } + } else { + return nil, nil, os.ErrNotExist } } lastSeg := segments[len(segments)-1] if parent.childs != nil { if node, ok := parent.childs[lastSeg]; ok { + if node.mode&os.ModeSymlink != 0 { + return fs.relativeFileInfo(parent, string(*node.buf)) + } return parent, node, nil } } else { @@ -211,10 +232,31 @@ func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err er return parent, nil, nil } +func (fs *MemFS) dirSegments(wd *fileInfo, path string) (parent *fileInfo, segments []string) { + path = filepath.Clean(path) + segments = vfs.SplitPath(path, PathSeparator) + + // Determine root to traverse + parent = fs.root + if segments[0] == "." { + parent = wd + } + segments = segments[1:] + return parent, segments +} + func hasFlag(flag int, flags int) bool { return flags&flag == flag } +// Open opens the named file on the given Filesystem for reading. +// If successful, methods on the returned file can be used for reading. +// The associated file descriptor has mode os.O_RDONLY. +// If there is an error, it will be of type *PathError. +func (fs *MemFS) Open(name string) (vfs.File, error) { + return fs.OpenFile(name, os.O_RDONLY, 0) +} + // OpenFile opens a file handle with a specified flag (os.O_RDONLY etc.) and perm (e.g. 0666). // If success the returned File can be used for I/O. Otherwise an error is returned, which // is a *os.PathError and can be extracted for further information. @@ -226,12 +268,12 @@ func (fs *MemFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, er base := filepath.Base(name) fiParent, fiNode, err := fs.fileInfo(name) if err != nil { - return nil, &os.PathError{"open", name, err} + return nil, &os.PathError{Op: "open", Path: name, Err: err} } if fiNode == nil { if !hasFlag(os.O_CREATE, flag) { - return nil, &os.PathError{"open", name, os.ErrNotExist} + return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} } fiNode = &fileInfo{ name: base, @@ -244,10 +286,10 @@ func (fs *MemFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, er fiParent.childs[base] = fiNode } else { // file exists if hasFlag(os.O_CREATE|os.O_EXCL, flag) { - return nil, &os.PathError{"open", name, os.ErrExist} + return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrExist} } if fiNode.dir { - return nil, &os.PathError{"open", name, ErrIsDirectory} + return nil, &os.PathError{Op: "open", Path: name, Err: ErrIsDirectory} } } @@ -270,9 +312,9 @@ func (fi *fileInfo) file(flag int) (vfs.File, error) { if hasFlag(os.O_RDWR, flag) { return f, nil } else if hasFlag(os.O_WRONLY, flag) { - f = &woFile{f} + f = &woFile{File: f} } else { - f = &roFile{f} + f = &roFile{File: f} } return f, nil @@ -307,10 +349,10 @@ func (fs *MemFS) Remove(name string) error { name = filepath.Clean(name) fiParent, fiNode, err := fs.fileInfo(name) if err != nil { - return &os.PathError{"remove", name, err} + return &os.PathError{Op: "remove", Path: name, Err: err} } if fiNode == nil { - return &os.PathError{"remove", name, os.ErrNotExist} + return &os.PathError{Op: "remove", Path: name, Err: os.ErrNotExist} } delete(fiParent.childs, fiNode.name) @@ -327,20 +369,20 @@ func (fs *MemFS) Rename(oldpath, newpath string) error { oldpath = filepath.Clean(oldpath) fiOldParent, fiOld, err := fs.fileInfo(oldpath) if err != nil { - return &os.PathError{"rename", oldpath, err} + return &os.PathError{Op: "rename", Path: oldpath, Err: err} } if fiOld == nil { - return &os.PathError{"rename", oldpath, os.ErrNotExist} + return &os.PathError{Op: "rename", Path: oldpath, Err: os.ErrNotExist} } newpath = filepath.Clean(newpath) fiNewParent, fiNew, err := fs.fileInfo(newpath) if err != nil { - return &os.PathError{"rename", newpath, err} + return &os.PathError{Op: "rename", Path: newpath, Err: err} } if fiNew != nil { - return &os.PathError{"rename", newpath, os.ErrExist} + return &os.PathError{Op: "rename", Path: newpath, Err: os.ErrExist} } newBase := filepath.Base(newpath) @@ -364,10 +406,10 @@ func (fs *MemFS) Stat(name string) (os.FileInfo, error) { // dir, base := filepath.Split(name) _, fi, err := fs.fileInfo(name) if err != nil { - return nil, &os.PathError{"stat", name, err} + return nil, &os.PathError{Op: "stat", Path: name, Err: err} } if fi == nil { - return nil, &os.PathError{"stat", name, os.ErrNotExist} + return nil, &os.PathError{Op: "stat", Path: name, Err: os.ErrNotExist} } return fi, nil } diff --git a/memfs/memfs_test.go b/memfs/memfs_test.go index d2f06d4..423451a 100644 --- a/memfs/memfs_test.go +++ b/memfs/memfs_test.go @@ -1,12 +1,13 @@ package memfs import ( - "io/ioutil" + "io" "os" + "strings" "testing" "time" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) func TestInterface(t *testing.T) { @@ -32,7 +33,6 @@ func TestCreate(t *testing.T) { if err != nil { t.Fatalf("Unexpected error creating file: %s", err) } - } // Create same file again, but truncate it @@ -69,7 +69,6 @@ func TestCreate(t *testing.T) { t.Errorf("Wrong name: %s", name) } } - } func TestMkdirAbsRel(t *testing.T) { @@ -123,9 +122,110 @@ func TestMkdirTree(t *testing.T) { t.Errorf("Expected error creating directory with non-existing parent") } - //TODO: Subdir of file + // TODO: Subdir of file +} + +func TestSymlink(t *testing.T) { + fs := Create() + + if err := vfs.MkdirAll(fs, "/tmp", 0755); err != nil { + t.Fatal("Unable to create /tmp:", err) + } + if err := vfs.WriteFile(fs, "/tmp/teacup", []byte("i am a teacup"), 0644); err != nil { + t.Fatal("Unable to fill teacup:", err) + } + err := fs.Symlink("/tmp/teacup", "/tmp/cup") + if err != nil { + t.Fatal("Symlink failed:", err) + } + fluid, err := vfs.ReadFile(fs, "/tmp/cup") + if err != nil { + t.Fatal("Failed to read from /tmp/cup:", err) + } + if string(fluid) != "i am a teacup" { + t.Fatal("Wrong contents in cup. got:", string(fluid)) + } } +func TestDirectorySymlink(t *testing.T) { + fs := Create() + + if err := vfs.MkdirAll(fs, "/foo/a/b", 0755); err != nil { + t.Fatal("Unable mkdir /foo/a/b:", err) + } + + if err := vfs.WriteFile(fs, "/foo/a/b/c", []byte("I can \"c\" clearly now"), 0644); err != nil { + t.Fatal("Unable to write /foo/a/b/c:", err) + } + + if err := fs.Symlink("/foo/a/b", "/foo/also_b"); err != nil { + t.Fatal("Unable to symlink /foo/also_b -> /foo/a/b:", err) + } + + contents, err := vfs.ReadFile(fs, "/foo/also_b/c") + if err != nil { + t.Fatal("Unable to read /foo/also_b/c:", err) + } + if string(contents) != "I can \"c\" clearly now" { + t.Fatal("Unexpected contents read from c:", err) + } +} + +func TestMultipleAndRelativeSymlinks(t *testing.T) { + fs := Create() + if err := vfs.MkdirAll(fs, "a/real_b/real_c", 0755); err != nil { + t.Fatal("Unable mkdir a/real_b/real_c:", err) + } + + for _, fsEntry := range []struct { + name, link, content string + }{ + {name: "a/b", link: "real_b"}, + {name: "a/b/c", link: "real_c"}, + {name: "a/b/c/real_d", content: "Lah dee dah"}, + {name: "a/b/c/d", link: "real_d"}, + {name: "a/d", link: "b/c/d"}, + } { + if fsEntry.link != "" { + if err := fs.Symlink(fsEntry.link, fsEntry.name); err != nil { + t.Fatalf("Unable to symlink %s -> %s: %v", fsEntry.name, fsEntry.link, err) + } + } else if fsEntry.content != "" { + if err := vfs.WriteFile(fs, fsEntry.name, []byte(fsEntry.content), 0644); err != nil { + t.Fatalf("Unable to write %s: %v", fsEntry.name, err) + } + } + } + + for _, fn := range []string{ + "a/b/c/d", + "a/d", + } { + contents, err := vfs.ReadFile(fs, fn) + if err != nil { + t.Fatalf("Unable to read %s: %v", fn, err) + } + if string(contents) != "Lah dee dah" { + t.Fatalf("Unexpected contents read from %s: %v", fn, err) + } + } +} + +func TestSymlinkIsNotADirectory(t *testing.T) { + fs := Create() + if err := vfs.MkdirAll(fs, "a/real_b/real_c", 0755); err != nil { + t.Fatal("Unable mkdir a/real_b/real_c:", err) + } + if err := fs.Symlink("broken", "a/b"); err != nil { + t.Fatal("Unable to symlink a/b -> broken:", err) + } + if err := vfs.WriteFile(fs, "a/b/c", []byte("Whatever"), 0644); !strings.Contains(err.Error(), vfs.ErrNotDirectory.Error()) { + t.Fatal("Expected an error when writing a/b/c:", err) + } +} + +// TODO: overwrite/remove symlinks + func TestReadDir(t *testing.T) { fs := Create() dirs := []string{"/home", "/home/linus", "/home/rob", "/home/pike", "/home/blang"} @@ -176,7 +276,6 @@ func TestReadDir(t *testing.T) { if _, err := fs.ReadDir("/usr"); err == nil { t.Errorf("Expected error readdir(nofound)") } - } func TestRemove(t *testing.T) { @@ -276,6 +375,25 @@ func TestReadWrite(t *testing.T) { } } +func TestOpen(t *testing.T) { + fs := Create() + f, err := fs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + t.Fatalf("OpenFile: %s", err) + } + if _, err = f.Write([]byte("test")); err != nil { + t.Fatalf("Write: %s", err) + } + f.Close() + f2, err := fs.Open("/readme.txt") + if err != nil { + t.Errorf("Open: %s", err) + } + if err := f2.Close(); err != nil { + t.Errorf("Close: %s", err) + } +} + func TestOpenRO(t *testing.T) { fs := Create() f, err := fs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDONLY, 0666) @@ -365,11 +483,11 @@ func TestTruncateToLength(t *testing.T) { size int64 err bool }{ - {-1, true}, - {0, false}, - {int64(len(dots) - 1), false}, - {int64(len(dots)), false}, - {int64(len(dots) + 1), false}, + {size: -1, err: true}, + {size: 0, err: false}, + {size: int64(len(dots) - 1), err: false}, + {size: int64(len(dots)), err: false}, + {size: int64(len(dots) + 1), err: false}, } for _, param := range params { fs := Create() @@ -495,7 +613,7 @@ func readFile(fs vfs.Filesystem, name string) ([]byte, error) { if err != nil { return nil, err } - return ioutil.ReadAll(f) + return io.ReadAll(f) } func TestRename(t *testing.T) { @@ -554,7 +672,6 @@ func TestRename(t *testing.T) { if err := fs.Rename("/newdirectory/README.txt", "/README.txt"); err == nil { t.Errorf("Expected error renaming file") } - } func TestModTime(t *testing.T) { diff --git a/mountfs/example_test.go b/mountfs/example_test.go index b9ce7b4..bb25620 100644 --- a/mountfs/example_test.go +++ b/mountfs/example_test.go @@ -1,9 +1,9 @@ package mountfs_test import ( - "github.com/blang/vfs" - "github.com/blang/vfs/memfs" - "github.com/blang/vfs/mountfs" + "github.com/3JoB/vfs" + "github.com/3JoB/vfs/memfs" + "github.com/3JoB/vfs/mountfs" ) func ExampleMountFS() { diff --git a/mountfs/mountfs.go b/mountfs/mountfs.go index aa0396c..9347d25 100644 --- a/mountfs/mountfs.go +++ b/mountfs/mountfs.go @@ -2,10 +2,11 @@ package mountfs import ( "errors" - "github.com/blang/vfs" "os" filepath "path" "strings" + + "github.com/3JoB/vfs" ) // ErrBoundary is returned if an operation @@ -127,6 +128,16 @@ func (fs MountFS) Mkdir(name string, perm os.FileMode) error { return mount.Mkdir(innerPath, perm) } +// Symlink creates a symlink +func (fs MountFS) Symlink(oldname, newname string) error { + oldMount, oldInnerName := findMount(oldname, fs.mounts, fs.rootFS, string(fs.PathSeparator())) + newMount, newInnerName := findMount(newname, fs.mounts, fs.rootFS, string(fs.PathSeparator())) + if oldMount != newMount { + return ErrBoundary + } + return oldMount.Symlink(oldInnerName, newInnerName) +} + type innerFileInfo struct { os.FileInfo name string @@ -136,6 +147,14 @@ func (fi innerFileInfo) Name() string { return fi.name } +// Open opens the named file on the given Filesystem for reading. +// If successful, methods on the returned file can be used for reading. +// The associated file descriptor has mode os.O_RDONLY. +// If there is an error, it will be of type *PathError. +func (fs MountFS) Open(name string) (vfs.File, error) { + return fs.OpenFile(name, os.O_RDONLY, 0) +} + // Stat returns the fileinfo of a file func (fs MountFS) Stat(name string) (os.FileInfo, error) { mount, innerPath := findMount(name, fs.mounts, fs.rootFS, string(fs.PathSeparator())) diff --git a/mountfs/mountfs_test.go b/mountfs/mountfs_test.go index 03ea839..50ac98f 100644 --- a/mountfs/mountfs_test.go +++ b/mountfs/mountfs_test.go @@ -2,9 +2,10 @@ package mountfs import ( "errors" - "github.com/blang/vfs" "os" "testing" + + "github.com/3JoB/vfs" ) type mountTest struct { @@ -19,31 +20,31 @@ type mtres struct { var mountTests = []mountTest{ { - []string{ + mounts: []string{ "/tmp", }, - map[string]mtres{ - "/": {"/", "/"}, - "/tmp": {"/tmp", "/"}, - "/tmp/test": {"/tmp", "/test"}, + results: map[string]mtres{ + "/": {mountPath: "/", innerPath: "/"}, + "/tmp": {mountPath: "/tmp", innerPath: "/"}, + "/tmp/test": {mountPath: "/tmp", innerPath: "/test"}, }, }, { - []string{ + mounts: []string{ "/home", "/home/user1", "/home/user2", }, - map[string]mtres{ - "/": {"/", "/"}, - "/tmp": {"/", "/tmp"}, - "/tmp/test": {"/", "/tmp/test"}, - "/home": {"/home", "/"}, - "/home/user1": {"/home/user1", "/"}, - "/home/user2": {"/home/user2", "/"}, - "/home/user1/subdir": {"/home/user1", "/subdir"}, - "/home/user2/subdir/subsubdir": {"/home/user2", "/subdir/subsubdir"}, + results: map[string]mtres{ + "/": {mountPath: "/", innerPath: "/"}, + "/tmp": {mountPath: "/", innerPath: "/tmp"}, + "/tmp/test": {mountPath: "/", innerPath: "/tmp/test"}, + "/home": {mountPath: "/home", innerPath: "/"}, + "/home/user1": {mountPath: "/home/user1", innerPath: "/"}, + "/home/user2": {mountPath: "/home/user2", innerPath: "/"}, + "/home/user1/subdir": {mountPath: "/home/user1", innerPath: "/subdir"}, + "/home/user2/subdir/subsubdir": {mountPath: "/home/user2", innerPath: "/subdir/subsubdir"}, }, }, } @@ -140,6 +141,9 @@ func TestOpenFile(t *testing.T) { if n := f.Name(); n != "/tmp/testfile" { t.Errorf("Unexpected filename: %s", n) } + if _, err := fs.Open("/tmp/testfile"); err != nil { + t.Errorf("Open: %s", err) + } } func (fs *testDummyFS) Mkdir(name string, perm os.FileMode) error { diff --git a/os.go b/os.go index feb33bb..4382510 100644 --- a/os.go +++ b/os.go @@ -18,6 +18,11 @@ func (fs OsFS) PathSeparator() uint8 { return os.PathSeparator } +// Open wraps os.Open +func (fs OsFS) Open(name string) (File, error) { + return os.Open(name) +} + // OpenFile wraps os.OpenFile func (fs OsFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { return os.OpenFile(name, flag, perm) @@ -28,11 +33,25 @@ func (fs OsFS) Remove(name string) error { return os.Remove(name) } +// RemoveAll removes path and any children it contains. +// It removes everything it can but returns the first error +// it encounters. If the path does not exist, RemoveAll +// returns nil (no error). +// If there is an error, it will be of type *PathError. +func (fs OsFS) RemoveAll(name string) error { + return os.RemoveAll(name) +} + // Mkdir wraps os.Mkdir func (fs OsFS) Mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } +// Symlink wraps os.Symlink +func (fs OsFS) Symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + // Rename wraps os.Rename func (fs OsFS) Rename(oldpath, newpath string) error { return os.Rename(oldpath, newpath) diff --git a/os_test.go b/os_test.go index 386cf73..4743726 100644 --- a/os_test.go +++ b/os_test.go @@ -16,12 +16,17 @@ func TestOSCreate(t *testing.T) { if err != nil { t.Errorf("Create: %s", err) } - err = f.Close() - if err != nil { + if err = f.Close(); err != nil { t.Errorf("Close: %s", err) } - err = fs.Remove(f.Name()) + f2, err := fs.Open("/tmp/test123") if err != nil { + t.Errorf("Open: %s", err) + } + if err := f2.Close(); err != nil { + t.Errorf("Close: %s", err) + } + if err := fs.Remove(f.Name()); err != nil { t.Errorf("Remove: %s", err) } } diff --git a/path.go b/path.go index 722e249..9330430 100644 --- a/path.go +++ b/path.go @@ -5,10 +5,12 @@ import ( ) // SplitPath splits the given path in segments: -// "/" -> []string{""} +// +// "/" -> []string{""} // "./file" -> []string{".", "file"} // "file" -> []string{".", "file"} // "/usr/src/linux/" -> []string{"", "usr", "src", "linux"} +// // The returned slice of path segments consists of one more more segments. func SplitPath(path string, sep string) []string { path = strings.TrimSpace(path) diff --git a/path_test.go b/path_test.go index 1c9fa5e..15eba9f 100644 --- a/path_test.go +++ b/path_test.go @@ -2,7 +2,6 @@ package vfs import ( "reflect" - "testing" ) diff --git a/prefixfs/prefixfs.go b/prefixfs/prefixfs.go index 6b84dfb..6b4d04e 100644 --- a/prefixfs/prefixfs.go +++ b/prefixfs/prefixfs.go @@ -3,7 +3,7 @@ package prefixfs import ( "os" - "github.com/blang/vfs" + "github.com/3JoB/vfs" ) // A FS that prefixes the path in each vfs.Filesystem operation. @@ -16,7 +16,7 @@ type FS struct { // Create returns a file system that prefixes all paths and forwards to root. func Create(root vfs.Filesystem, prefix string) *FS { - return &FS{root, prefix} + return &FS{Filesystem: root, Prefix: prefix} } // PrefixPath returns path with the prefix prefixed. @@ -47,6 +47,11 @@ func (fs *FS) Mkdir(name string, perm os.FileMode) error { return fs.Filesystem.Mkdir(fs.PrefixPath(name), perm) } +// Symlink implements vfs.Filesystem. +func (fs *FS) Symlink(oldname, newname string) error { + return fs.Filesystem.Symlink(fs.PrefixPath(oldname), fs.PrefixPath(newname)) +} + // Stat implements vfs.Filesystem. func (fs *FS) Stat(name string) (os.FileInfo, error) { return fs.Filesystem.Stat(fs.PrefixPath(name)) diff --git a/prefixfs/prefixfs_test.go b/prefixfs/prefixfs_test.go index c04e746..963025f 100644 --- a/prefixfs/prefixfs_test.go +++ b/prefixfs/prefixfs_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - "github.com/blang/vfs" - "github.com/blang/vfs/memfs" + "github.com/3JoB/vfs" + "github.com/3JoB/vfs/memfs" ) const prefixPath = "/prefix" @@ -35,13 +35,12 @@ func TestOpenFile(t *testing.T) { fs := Create(rfs, prefixPath) f, err := fs.OpenFile("file", os.O_CREATE, 0666) - defer f.Close() if err != nil { t.Errorf("OpenFile: %v", err) } + defer f.Close() - _, err = rfs.Stat(prefix("file")) - if os.IsNotExist(err) { + if _, err := rfs.Stat(prefix("file")); os.IsNotExist(err) { t.Errorf("root:%v not found (%v)", prefix("file"), err) } } @@ -51,18 +50,16 @@ func TestRemove(t *testing.T) { fs := Create(rfs, prefixPath) f, err := fs.OpenFile("file", os.O_CREATE, 0666) - defer f.Close() if err != nil { t.Errorf("OpenFile: %v", err) } + defer f.Close() - err = fs.Remove("file") - if err != nil { + if err := fs.Remove("file"); err != nil { t.Errorf("Remove: %v", err) } - _, err = rfs.Stat(prefix("file")) - if os.IsExist(err) { + if _, err := rfs.Stat(prefix("file")); os.IsExist(err) { t.Errorf("root:%v found (%v)", prefix("file"), err) } } @@ -72,18 +69,16 @@ func TestRename(t *testing.T) { fs := Create(rfs, prefixPath) f, err := fs.OpenFile("file", os.O_CREATE, 0666) - defer f.Close() if err != nil { t.Errorf("OpenFile: %v", err) } + defer f.Close() - err = fs.Rename("file", "file2") - if err != nil { + if err := fs.Rename("file", "file2"); err != nil { t.Errorf("Rename: %v", err) } - _, err = rfs.Stat(prefix("file2")) - if os.IsNotExist(err) { + if _, err = rfs.Stat(prefix("file2")); os.IsNotExist(err) { t.Errorf("root:%v not found (%v)", prefix("file2"), err) } } @@ -92,26 +87,43 @@ func TestMkdir(t *testing.T) { rfs := rootfs() fs := Create(rfs, prefixPath) - err := fs.Mkdir("dir", 0777) - if err != nil { + if err := fs.Mkdir("dir", 0777); err != nil { t.Errorf("Mkdir: %v", err) } - _, err = rfs.Stat(prefix("dir")) - if os.IsNotExist(err) { + if _, err := rfs.Stat(prefix("dir")); os.IsNotExist(err) { t.Errorf("root:%v not found (%v)", prefix("dir"), err) } } -func TestStat(t *testing.T) { +func TestSymlink(t *testing.T) { rfs := rootfs() fs := Create(rfs, prefixPath) f, err := fs.OpenFile("file", os.O_CREATE, 0666) + if err != nil { + t.Errorf("OpenFile: %v", err) + } defer f.Close() + + if err = fs.Symlink("/file", "file2"); err != nil { + t.Errorf("Symlink: %v", err) + } + + if _, err = rfs.Stat(prefix("file2")); os.IsNotExist(err) { + t.Errorf("root:%v not found (%v)", prefix("file2"), err) + } +} + +func TestStat(t *testing.T) { + rfs := rootfs() + fs := Create(rfs, prefixPath) + + f, err := fs.OpenFile("file", os.O_CREATE, 0666) if err != nil { t.Errorf("OpenFile: %v", err) } + defer f.Close() fi, err := fs.Stat("file") if os.IsNotExist(err) { @@ -153,10 +165,10 @@ func TestLstat(t *testing.T) { fs := Create(rfs, prefixPath) f, err := fs.OpenFile("file", os.O_CREATE, 0666) - defer f.Close() if err != nil { t.Errorf("OpenFile: %v", err) } + defer f.Close() fi, err := fs.Lstat("file") if os.IsNotExist(err) { @@ -197,21 +209,19 @@ func TestReadDir(t *testing.T) { rfs := rootfs() fs := Create(rfs, prefixPath) - err := fs.Mkdir("dir", 0777) - if err != nil { + if err := fs.Mkdir("dir", 0777); err != nil { t.Errorf("Mkdir: %v", err) } - _, err = rfs.Stat(prefix("dir")) - if os.IsNotExist(err) { + if _, err := rfs.Stat(prefix("dir")); os.IsNotExist(err) { t.Errorf("root:%v not found (%v)", prefix("dir"), err) } f, err := fs.OpenFile("dir/file", os.O_CREATE, 0666) - defer f.Close() if err != nil { t.Errorf("OpenFile: %v", err) } + defer f.Close() s, err := fs.ReadDir("dir") if err != nil { diff --git a/readonly.go b/readonly.go index 406cd84..35ba739 100644 --- a/readonly.go +++ b/readonly.go @@ -3,15 +3,16 @@ package vfs import ( "errors" "os" + ) // ReadOnly creates a readonly wrapper around the given filesystem. // It disables the following operations: // -// - Create -// - Remove -// - Rename -// - Mkdir +// - Create +// - Remove +// - Rename +// - Mkdir // // And disables OpenFile flags: os.O_CREATE, os.O_APPEND, os.O_WRONLY // @@ -44,6 +45,18 @@ func (fs RoFS) Mkdir(name string, perm os.FileMode) error { return ErrReadOnly } +func (fs RoFS) Symlink(oldname, newname string) error { + return ErrReadOnly +} + +// Open opens the named file on the given Filesystem for reading. +// If successful, methods on the returned file can be used for reading. +// The associated file descriptor has mode os.O_RDONLY. +// If there is an error, it will be of type *PathError. +func (fs RoFS) Open(name string) (File, error) { + return fs.OpenFile(name, os.O_RDONLY, 0) +} + // OpenFile returns ErrorReadOnly if flag contains os.O_CREATE, os.O_APPEND, os.O_WRONLY. // Otherwise it returns a read-only File with disabled Write(..) operation. func (fs RoFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { @@ -65,7 +78,7 @@ func (fs RoFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { // ReadOnlyFile wraps the given file and disables Write(..) operation. func ReadOnlyFile(f File) File { - return &roFile{f} + return &roFile{File: f} } type roFile struct { diff --git a/readonly_test.go b/readonly_test.go index f1ca36e..96d1441 100644 --- a/readonly_test.go +++ b/readonly_test.go @@ -8,6 +8,7 @@ import ( var ( errDummy = errors.New("Not implemented") + // Complete dummy base baseFSDummy = Dummy(errDummy) ro = ReadOnly(baseFSDummy) @@ -66,6 +67,13 @@ func TestMkDir(t *testing.T) { } } +func TestROSymlink(t *testing.T) { + err := ro.Symlink("old", "new") + if err != ErrReadOnly { + t.Errorf("Symlink error expected") + } +} + type writeDummyFS struct { Filesystem } @@ -77,7 +85,7 @@ func (fs writeDummyFS) OpenFile(name string, flag int, perm os.FileMode) (File, func TestROOpenFileWrite(t *testing.T) { // Dummy base with mocked OpenFile for write test - roWriteMock := ReadOnly(writeDummyFS{Dummy(errDummy)}) + roWriteMock := ReadOnly(writeDummyFS{Filesystem: Dummy(errDummy)}) f, err := roWriteMock.OpenFile("name", os.O_RDWR, 0) if err != nil { diff --git a/walk.go b/walk.go new file mode 100644 index 0000000..a889792 --- /dev/null +++ b/walk.go @@ -0,0 +1,80 @@ +package vfs + +import ( + "os" + "path/filepath" + "sort" + "strings" +) + +// Walk walks the file tree rooted at root, calling walkFunc for each file or +// directory in the tree, including root. All errors that arise visiting files +// and directories are filtered by walkFn. The files are walked in lexical +// order, which makes the output deterministic but means that for very +// large directories Walk can be inefficient. +// Walk does not follow symbolic links. +func Walk(fs Filesystem, root string, walkFunc filepath.WalkFunc) error { + info, err := fs.Lstat(root) + if err != nil { + err = walkFunc(root, nil, err) + } else { + err = walk(fs, root, info, walkFunc) + } + if err == filepath.SkipDir { + return nil + } + return err +} + +// readDirNames reads the directory named by dirname and returns +// a sorted list of directory entries. +func readDirNames(fs Filesystem, dirname string) ([]string, error) { + infos, err := fs.ReadDir(dirname) + if err != nil { + return nil, err + } + names := make([]string, 0, len(infos)) + for _, info := range infos { + names = append(names, info.Name()) + } + sort.Strings(names) + return names, nil +} + +// walk recursively descends path, calling walkFunc. +func walk(fs Filesystem, path string, info os.FileInfo, walkFunc filepath.WalkFunc) error { + if !info.IsDir() { + return walkFunc(path, info, nil) + } + + names, err := readDirNames(fs, path) + err1 := walkFunc(path, info, err) + // If err != nil, walk can't walk into this directory. + // err1 != nil means walkFn want walk to skip this directory or stop walking. + // Therefore, if one of err and err1 isn't nil, walk will return. + if err != nil || err1 != nil { + // The caller's behavior is controlled by the return value, which is decided + // by walkFn. walkFn may ignore err and return nil. + // If walkFn returns SkipDir, it will be handled by the caller. + // So walk should return whatever walkFn returns. + return err1 + } + + for _, name := range names { + filename := strings.Join([]string{path, name}, string(fs.PathSeparator())) + fileInfo, err := fs.Lstat(filename) + if err != nil { + if err := walkFunc(filename, fileInfo, err); err != nil && err != filepath.SkipDir { + return err + } + } else { + err = walk(fs, filename, fileInfo, walkFunc) + if err != nil { + if !fileInfo.IsDir() || err != filepath.SkipDir { + return err + } + } + } + } + return nil +} diff --git a/walk_test.go b/walk_test.go new file mode 100644 index 0000000..9afa907 --- /dev/null +++ b/walk_test.go @@ -0,0 +1,320 @@ +package vfs + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +type Node struct { + name string + entries []*Node // nil if the entry is a file + mark int +} + +var tree = &Node{ + name: "testdata", + entries: []*Node{ + {"a", nil, 0}, + {"b", []*Node{}, 0}, + {"c", nil, 0}, + { + "d", + []*Node{ + {"x", nil, 0}, + {"y", []*Node{}, 0}, + { + "z", + []*Node{ + {"u", nil, 0}, + {"v", nil, 0}, + }, + 0, + }, + }, + 0, + }, + }, + mark: 0, +} + +func walkTree(n *Node, path string, f func(path string, n *Node)) { + f(path, n) + for _, e := range n.entries { + walkTree(e, filepath.Join(path, e.name), f) + } +} + +func makeTree(t *testing.T, fs Filesystem) { + walkTree(tree, tree.name, func(path string, n *Node) { + if n.entries == nil { + fd, err := fs.OpenFile(path, os.O_CREATE, os.ModePerm) + if err != nil { + t.Errorf("makeTree: %v", err) + return + } + fd.Close() + } else { + fs.Mkdir(path, 0770) + } + }) +} + +func markTree(n *Node) { walkTree(n, "", func(path string, n *Node) { n.mark++ }) } + +func checkMarks(t *testing.T, report bool) { + walkTree(tree, tree.name, func(path string, n *Node) { + if n.mark != 1 && report { + t.Errorf("node %s mark = %d; expected 1", path, n.mark) + } + n.mark = 0 + }) +} + +// Assumes that each node name is unique. Good enough for a test. +// If clear is true, any incoming error is cleared before return. The errors +// are always accumulated, though. +func mark(info os.FileInfo, err error, errors *[]error, clear bool) error { + name := info.Name() + walkTree(tree, tree.name, func(path string, n *Node) { + if n.name == name { + n.mark++ + } + }) + if err != nil { + *errors = append(*errors, err) + if clear { + return nil + } + return err + } + return nil +} + +func chtmpdir(t *testing.T) (restore func()) { + oldwd, err := os.Getwd() + if err != nil { + t.Fatalf("chtmpdir: %v", err) + } + d, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatalf("chtmpdir: %v", err) + } + if err := os.Chdir(d); err != nil { + t.Fatalf("chtmpdir: %v", err) + } + return func() { + if err := os.Chdir(oldwd); err != nil { + t.Fatalf("chtmpdir: %v", err) + } + os.RemoveAll(d) + } +} + +func TestWalk(t *testing.T) { + if runtime.GOOS == "darwin" { + switch runtime.GOARCH { + case "arm", "arm64": + restore := chtmpdir(t) + defer restore() + } + } + + tmpDir, err := os.MkdirTemp("", "TestWalk") + if err != nil { + t.Fatal("creating temp dir:", err) + } + defer os.RemoveAll(tmpDir) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal("finding working dir:", err) + } + if err = os.Chdir(tmpDir); err != nil { + t.Fatal("entering temp dir:", err) + } + defer os.Chdir(origDir) + + fs := OS() + + makeTree(t, fs) + errors := make([]error, 0, 10) + clear := true + markFn := func(path string, info os.FileInfo, err error) error { + return mark(info, err, &errors, clear) + } + // Expect no errors. + err = Walk(fs, tree.name, markFn) + if err != nil { + t.Fatalf("no error expected, found: %s", err) + } + if len(errors) != 0 { + t.Fatalf("unexpected errors: %s", errors) + } + checkMarks(t, true) + errors = errors[0:0] + + // Test permission errors. Only possible if we're not root + // and only on some file systems (AFS, FAT). To avoid errors during + // all.bash on those file systems, skip during go test -short. + if os.Getuid() > 0 && !testing.Short() { + // introduce 2 errors: chmod top-level directories to 0 + os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0) + os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0) + + // 3) capture errors, expect two. + // mark respective subtrees manually + markTree(tree.entries[1]) + markTree(tree.entries[3]) + // correct double-marking of directory itself + tree.entries[1].mark-- + tree.entries[3].mark-- + err := Walk(fs, tree.name, markFn) + if err != nil { + t.Fatalf("expected no error return from Walk, got %s", err) + } + if len(errors) != 2 { + t.Errorf("expected 2 errors, got %d: %s", len(errors), errors) + } + // the inaccessible subtrees were marked manually + checkMarks(t, true) + errors = errors[0:0] + + // 4) capture errors, stop after first error. + // mark respective subtrees manually + markTree(tree.entries[1]) + markTree(tree.entries[3]) + // correct double-marking of directory itself + tree.entries[1].mark-- + tree.entries[3].mark-- + clear = false // error will stop processing + err = Walk(fs, tree.name, markFn) + if err == nil { + t.Fatalf("expected error return from Walk") + } + if len(errors) != 1 { + t.Errorf("expected 1 error, got %d: %s", len(errors), errors) + } + // the inaccessible subtrees were marked manually + checkMarks(t, false) + errors = errors[0:0] + + // restore permissions + os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0770) + os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0770) + } +} + +func touch(t *testing.T, fs Filesystem, name string) { + f, err := fs.OpenFile(name, os.O_CREATE, os.ModePerm) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } +} + +func TestWalkSkipDirOnFile(t *testing.T) { + td, err := os.MkdirTemp("", "walktest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + if err := os.MkdirAll(filepath.Join(td, "dir"), 0755); err != nil { + t.Fatal(err) + } + + fs := OS() + + touch(t, fs, filepath.Join(td, "dir/foo1")) + touch(t, fs, filepath.Join(td, "dir/foo2")) + + sawFoo2 := false + walker := func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(path, "foo2") { + sawFoo2 = true + } + if strings.HasSuffix(path, "foo1") { + return filepath.SkipDir + } + return nil + } + + err = Walk(fs, td, walker) + if err != nil { + t.Fatal(err) + } + if sawFoo2 { + t.Errorf("SkipDir on file foo1 did not block processing of foo2") + } + + err = Walk(fs, filepath.Join(td, "dir"), walker) + if err != nil { + t.Fatal(err) + } + if sawFoo2 { + t.Errorf("SkipDir on file foo1 did not block processing of foo2") + } +} + +type statWrapper struct { + Filesystem + + statErr error +} + +func (s *statWrapper) Lstat(path string) (os.FileInfo, error) { + if strings.HasSuffix(path, "stat-error") { + return nil, s.statErr + } + return os.Lstat(path) +} + +func TestWalkFileError(t *testing.T) { + td, err := os.MkdirTemp("", "walktest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + fs := Filesystem(OS()) + + touch(t, fs, filepath.Join(td, "foo")) + touch(t, fs, filepath.Join(td, "bar")) + dir := filepath.Join(td, "dir") + if err := MkdirAll(fs, filepath.Join(td, "dir"), 0755); err != nil { + t.Fatal(err) + } + touch(t, fs, filepath.Join(dir, "baz")) + touch(t, fs, filepath.Join(dir, "stat-error")) + statErr := errors.New("some stat error") + + fs = &statWrapper{Filesystem: fs, statErr: statErr} + + got := map[string]error{} + err = Walk(fs, td, func(path string, fi os.FileInfo, err error) error { + rel, _ := filepath.Rel(td, path) + got[filepath.ToSlash(rel)] = err + return nil + }) + if err != nil { + t.Errorf("Walk error: %v", err) + } + want := map[string]error{ + ".": nil, + "foo": nil, + "bar": nil, + "dir": nil, + "dir/baz": nil, + "dir/stat-error": statErr, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Walked %#v; want %#v", got, want) + } +}