From c597fe5aa8d940f59ae649c130ab6140324c8471 Mon Sep 17 00:00:00 2001 From: Vladimir Stolyarov Date: Sat, 13 Mar 2021 20:53:09 +0300 Subject: [PATCH] Add adapter to allow afero.Fs usage as io/fs.FS It will not affect users of go older than 1.16 because files with `io/fs` import marked with build constraints --- .travis.yml | 1 + README.md | 2 +- iofs.go | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++ iofs_test.go | 53 ++++++++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 iofs.go create mode 100644 iofs_test.go diff --git a/.travis.yml b/.travis.yml index a3c5906..e944f59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ arch: go: - "1.14" - "1.15" + - "1.16" - tip os: diff --git a/README.md b/README.md index 1400bc4..fb8eaaf 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ filesystem for full interoperability. * Support for compositional (union) file systems by combining multiple file systems acting as one * Specialized backends which modify existing filesystems (Read Only, Regexp filtered) * A set of utility functions ported from io, ioutil & hugo to be afero aware - +* Wrapper for go 1.16 filesystem abstraction `io/fs.FS` # Using Afero diff --git a/iofs.go b/iofs.go new file mode 100644 index 0000000..77f62c6 --- /dev/null +++ b/iofs.go @@ -0,0 +1,138 @@ +// +build go1.16 + +package afero + +import ( + "io/fs" + "path" +) + +// IOFS adopts afero.Fs to stdlib io/fs.FS +type IOFS struct { + Fs +} + +func NewIOFS(fs Fs) IOFS { + return IOFS{Fs: fs} +} + +var ( + _ fs.FS = IOFS{} + _ fs.GlobFS = IOFS{} + _ fs.ReadDirFS = IOFS{} + _ fs.ReadFileFS = IOFS{} + _ fs.StatFS = IOFS{} + _ fs.SubFS = IOFS{} +) + +func (iofs IOFS) Open(name string) (fs.File, error) { + const op = "open" + + // by convention for fs.FS implementations we should perform this check + if !fs.ValidPath(name) { + return nil, iofs.wrapError(op, name, fs.ErrInvalid) + } + + file, err := iofs.Fs.Open(name) + if err != nil { + return nil, iofs.wrapError(op, name, err) + } + + // file should implement fs.ReadDirFile + if _, ok := file.(fs.ReadDirFile); !ok { + file = readDirFile{file} + } + + return file, nil +} + +func (iofs IOFS) Glob(pattern string) ([]string, error) { + const op = "glob" + + // afero.Glob does not perform this check but it's required for implementations + if _, err := path.Match(pattern, ""); err != nil { + return nil, iofs.wrapError(op, pattern, err) + } + + items, err := Glob(iofs.Fs, pattern) + if err != nil { + return nil, iofs.wrapError(op, pattern, err) + } + + return items, nil +} + +func (iofs IOFS) ReadDir(name string) ([]fs.DirEntry, error) { + items, err := ReadDir(iofs.Fs, name) + if err != nil { + return nil, iofs.wrapError("readdir", name, err) + } + + ret := make([]fs.DirEntry, len(items)) + for i := range items { + ret[i] = dirEntry{items[i]} + } + + return ret, nil +} + +func (iofs IOFS) ReadFile(name string) ([]byte, error) { + const op = "readfile" + + if !fs.ValidPath(name) { + return nil, iofs.wrapError(op, name, fs.ErrInvalid) + } + + bytes, err := ReadFile(iofs.Fs, name) + if err != nil { + return nil, iofs.wrapError(op, name, err) + } + + return bytes, nil +} + +func (iofs IOFS) Sub(dir string) (fs.FS, error) { return IOFS{NewBasePathFs(iofs.Fs, dir)}, nil } + +func (IOFS) wrapError(op, path string, err error) error { + if _, ok := err.(*fs.PathError); ok { + return err // don't need to wrap again + } + + return &fs.PathError{ + Op: op, + Path: path, + Err: err, + } +} + +// dirEntry provides adapter from os.FileInfo to fs.DirEntry +type dirEntry struct { + fs.FileInfo +} + +var _ fs.DirEntry = dirEntry{} + +func (d dirEntry) Type() fs.FileMode { return d.FileInfo.Mode().Type() } + +func (d dirEntry) Info() (fs.FileInfo, error) { return d.FileInfo, nil } + +// readDirFile provides adapter from afero.File to fs.ReadDirFile needed for correct Open +type readDirFile struct { + File +} + +var _ fs.ReadDirFile = readDirFile{} + +func (r readDirFile) ReadDir(n int) ([]fs.DirEntry, error) { + items, err := r.File.Readdir(n) + if err != nil { + return nil, err + } + + ret := make([]fs.DirEntry, len(items)) + for i := range items { + ret[i] = dirEntry{items[i]} + } + + return ret, nil +} diff --git a/iofs_test.go b/iofs_test.go new file mode 100644 index 0000000..bd4893b --- /dev/null +++ b/iofs_test.go @@ -0,0 +1,53 @@ +// +build go1.16 + +package afero + +import ( + "os" + "testing" + "testing/fstest" +) + +func TestIOFS(t *testing.T) { + t.Parallel() + + t.Run("use MemMapFs", func(t *testing.T) { + mmfs := NewMemMapFs() + + err := mmfs.MkdirAll("dir1/dir2", os.ModePerm) + if err != nil { + t.Fatal("MkdirAll failed:", err) + } + + f, err := mmfs.OpenFile("dir1/dir2/test.txt", os.O_RDWR|os.O_CREATE, os.ModePerm) + if err != nil { + t.Fatal("OpenFile (O_CREATE) failed:", err) + } + + f.Close() + + if err := fstest.TestFS(NewIOFS(mmfs), "dir1/dir2/test.txt"); err != nil { + t.Error(err) + } + }) + + t.Run("use OsFs", func(t *testing.T) { + osfs := NewBasePathFs(NewOsFs(), t.TempDir()) + + err := osfs.MkdirAll("dir1/dir2", os.ModePerm) + if err != nil { + t.Fatal("MkdirAll failed:", err) + } + + f, err := osfs.OpenFile("dir1/dir2/test.txt", os.O_RDWR|os.O_CREATE, os.ModePerm) + if err != nil { + t.Fatal("OpenFile (O_CREATE) failed:", err) + } + + f.Close() + + if err := fstest.TestFS(NewIOFS(osfs), "dir1/dir2/test.txt"); err != nil { + t.Error(err) + } + }) +}