forked from mirror/afero
Add reverse wrapper from io.FS to afero.Fs
afero.Fs/File requires mutating methods implementation but they will always return error because fs.FS is read-only. ReadAt and Seek will return error if underlying fs.File doesn't implement them.
This commit is contained in:
parent
c597fe5aa8
commit
164e24d3b1
150
iofs.go
150
iofs.go
|
@ -3,8 +3,11 @@
|
|||
package afero
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IOFS adopts afero.Fs to stdlib io/fs.FS
|
||||
|
@ -136,3 +139,150 @@ func (r readDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
|||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// FromIOFS adopts io/fs.FS to use it as afero.Fs
|
||||
// Note that io/fs.FS is read-only so all mutating methods will return fs.PathError with fs.ErrPermission
|
||||
// To store modifications you may use afero.CopyOnWriteFs
|
||||
type FromIOFS struct {
|
||||
fs.FS
|
||||
}
|
||||
|
||||
var _ Fs = FromIOFS{}
|
||||
|
||||
func (f FromIOFS) Create(name string) (File, error) { return nil, notImplemented("create", name) }
|
||||
|
||||
func (f FromIOFS) Mkdir(name string, perm os.FileMode) error { return notImplemented("mkdir", name) }
|
||||
|
||||
func (f FromIOFS) MkdirAll(path string, perm os.FileMode) error {
|
||||
return notImplemented("mkdirall", path)
|
||||
}
|
||||
|
||||
func (f FromIOFS) Open(name string) (File, error) {
|
||||
file, err := f.FS.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fromIOFSFile{File: file, name: name}, nil
|
||||
}
|
||||
|
||||
func (f FromIOFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||
return f.Open(name)
|
||||
}
|
||||
|
||||
func (f FromIOFS) Remove(name string) error {
|
||||
return notImplemented("remove", name)
|
||||
}
|
||||
|
||||
func (f FromIOFS) RemoveAll(path string) error {
|
||||
return notImplemented("removeall", path)
|
||||
}
|
||||
|
||||
func (f FromIOFS) Rename(oldname, newname string) error {
|
||||
return notImplemented("rename", oldname)
|
||||
}
|
||||
|
||||
func (f FromIOFS) Stat(name string) (os.FileInfo, error) { return fs.Stat(f.FS, name) }
|
||||
|
||||
func (f FromIOFS) Name() string { return "fromiofs" }
|
||||
|
||||
func (f FromIOFS) Chmod(name string, mode os.FileMode) error {
|
||||
return notImplemented("chmod", name)
|
||||
}
|
||||
|
||||
func (f FromIOFS) Chown(name string, uid, gid int) error {
|
||||
return notImplemented("chown", name)
|
||||
}
|
||||
|
||||
func (f FromIOFS) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
return notImplemented("chtimes", name)
|
||||
}
|
||||
|
||||
type fromIOFSFile struct {
|
||||
fs.File
|
||||
name string
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
readerAt, ok := f.File.(io.ReaderAt)
|
||||
if !ok {
|
||||
return -1, notImplemented("readat", f.name)
|
||||
}
|
||||
|
||||
return readerAt.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) Seek(offset int64, whence int) (int64, error) {
|
||||
seeker, ok := f.File.(io.Seeker)
|
||||
if !ok {
|
||||
return -1, notImplemented("seek", f.name)
|
||||
}
|
||||
|
||||
return seeker.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) Write(p []byte) (n int, err error) {
|
||||
return -1, notImplemented("write", f.name)
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
return -1, notImplemented("writeat", f.name)
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) Name() string { return f.name }
|
||||
|
||||
func (f fromIOFSFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
rdfile, ok := f.File.(fs.ReadDirFile)
|
||||
if !ok {
|
||||
return nil, notImplemented("readdir", f.name)
|
||||
}
|
||||
|
||||
entries, err := rdfile.ReadDir(count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]os.FileInfo, len(entries))
|
||||
for i := range entries {
|
||||
ret[i], err = entries[i].Info()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) Readdirnames(n int) ([]string, error) {
|
||||
rdfile, ok := f.File.(fs.ReadDirFile)
|
||||
if !ok {
|
||||
return nil, notImplemented("readdir", f.name)
|
||||
}
|
||||
|
||||
entries, err := rdfile.ReadDir(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]string, len(entries))
|
||||
for i := range entries {
|
||||
ret[i] = entries[i].Name()
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) Sync() error { return nil }
|
||||
|
||||
func (f fromIOFSFile) Truncate(size int64) error {
|
||||
return notImplemented("truncate", f.name)
|
||||
}
|
||||
|
||||
func (f fromIOFSFile) WriteString(s string) (ret int, err error) {
|
||||
return -1, notImplemented("writestring", f.name)
|
||||
}
|
||||
|
||||
func notImplemented(op, path string) error {
|
||||
return &fs.PathError{Op: op, Path: path, Err: fs.ErrPermission}
|
||||
}
|
||||
|
|
359
iofs_test.go
359
iofs_test.go
|
@ -3,9 +3,14 @@
|
|||
package afero
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIOFS(t *testing.T) {
|
||||
|
@ -51,3 +56,357 @@ func TestIOFS(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIOFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fsys := fstest.MapFS{
|
||||
"test.txt": {
|
||||
Data: []byte("File in root"),
|
||||
Mode: fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
"dir1": {
|
||||
Mode: fs.ModeDir | fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
"dir1/dir2": {
|
||||
Mode: fs.ModeDir | fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
"dir1/dir2/hello.txt": {
|
||||
Data: []byte("Hello world"),
|
||||
Mode: fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
fromIOFS := FromIOFS{fsys}
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
_, err := fromIOFS.Create("test")
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Mkdir", func(t *testing.T) {
|
||||
err := fromIOFS.Mkdir("test", 0)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("MkdirAll", func(t *testing.T) {
|
||||
err := fromIOFS.Mkdir("test", 0)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Open", func(t *testing.T) {
|
||||
t.Run("non existing file", func(t *testing.T) {
|
||||
_, err := fromIOFS.Open("nonexisting")
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Errorf("Expected error to be fs.ErrNotExist, got %[1]T (%[1]v)", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory", func(t *testing.T) {
|
||||
dirFile, err := fromIOFS.Open("dir1")
|
||||
if err != nil {
|
||||
t.Errorf("dir1 open failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer dirFile.Close()
|
||||
|
||||
dirStat, err := dirFile.Stat()
|
||||
if err != nil {
|
||||
t.Errorf("dir1 stat failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !dirStat.IsDir() {
|
||||
t.Errorf("dir1 stat told that it is not a directory")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("simple file", func(t *testing.T) {
|
||||
file, err := fromIOFS.Open("test.txt")
|
||||
if err != nil {
|
||||
t.Errorf("test.txt open failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
fileStat, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Errorf("test.txt stat failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if fileStat.IsDir() {
|
||||
t.Errorf("test.txt stat told that it is a directory")
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
err := fromIOFS.Remove("test")
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Rename", func(t *testing.T) {
|
||||
err := fromIOFS.Rename("test", "test2")
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Stat", func(t *testing.T) {
|
||||
t.Run("non existing file", func(t *testing.T) {
|
||||
_, err := fromIOFS.Stat("nonexisting")
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Errorf("Expected error to be fs.ErrNotExist, got %[1]T (%[1]v)", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory", func(t *testing.T) {
|
||||
stat, err := fromIOFS.Stat("dir1/dir2")
|
||||
if err != nil {
|
||||
t.Errorf("dir1/dir2 stat failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
t.Errorf("dir1/dir2 stat told that it is not a directory")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file", func(t *testing.T) {
|
||||
stat, err := fromIOFS.Stat("dir1/dir2/hello.txt")
|
||||
if err != nil {
|
||||
t.Errorf("dir1/dir2 stat failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
t.Errorf("dir1/dir2/hello.txt stat told that it is a directory")
|
||||
return
|
||||
}
|
||||
|
||||
if lenFile := len(fsys["dir1/dir2/hello.txt"].Data); int64(lenFile) != stat.Size() {
|
||||
t.Errorf("dir1/dir2/hello.txt stat told invalid size: expected %d, got %d", lenFile, stat.Size())
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Chmod", func(t *testing.T) {
|
||||
err := fromIOFS.Chmod("test", os.ModePerm)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Chown", func(t *testing.T) {
|
||||
err := fromIOFS.Chown("test", 0, 0)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Chtimes", func(t *testing.T) {
|
||||
err := fromIOFS.Chtimes("test", time.Now(), time.Now())
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIOFS_File(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fsys := fstest.MapFS{
|
||||
"test.txt": {
|
||||
Data: []byte("File in root"),
|
||||
Mode: fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
"dir1": {
|
||||
Mode: fs.ModeDir | fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
"dir2": {
|
||||
Mode: fs.ModeDir | fs.ModePerm,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
fromIOFS := FromIOFS{fsys}
|
||||
|
||||
file, err := fromIOFS.Open("test.txt")
|
||||
if err != nil {
|
||||
t.Errorf("test.txt open failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
fileStat, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Errorf("test.txt stat failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if fileStat.IsDir() {
|
||||
t.Errorf("test.txt stat told that it is a directory")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("ReadAt", func(t *testing.T) {
|
||||
// MapFS files implements io.ReaderAt
|
||||
b := make([]byte, 2)
|
||||
_, err := file.ReadAt(b, 2)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ReadAt failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if expectedData := fsys["test.txt"].Data[2:4]; !bytes.Equal(b, expectedData) {
|
||||
t.Errorf("Unexpected content read: %s, expected %s", b, expectedData)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Seek", func(t *testing.T) {
|
||||
n, err := file.Seek(2, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Errorf("Seek failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if n != 2 {
|
||||
t.Errorf("Seek returned unexpected value: %d, expected 2", n)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write", func(t *testing.T) {
|
||||
_, err := file.Write(nil)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("WriteAt", func(t *testing.T) {
|
||||
_, err := file.WriteAt(nil, 0)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
if name := file.Name(); name != "test.txt" {
|
||||
t.Errorf("expected file.Name() == test.txt, got %s", name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Readdir", func(t *testing.T) {
|
||||
t.Run("not directory", func(t *testing.T) {
|
||||
_, err := file.Readdir(-1)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("root directory", func(t *testing.T) {
|
||||
root, err := fromIOFS.Open(".")
|
||||
if err != nil {
|
||||
t.Errorf("root open failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer root.Close()
|
||||
|
||||
items, err := root.Readdir(-1)
|
||||
if err != nil {
|
||||
t.Errorf("Readdir error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var expectedItems = []struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
Size int64
|
||||
}{
|
||||
{Name: "dir1", IsDir: true, Size: 0},
|
||||
{Name: "dir2", IsDir: true, Size: 0},
|
||||
{Name: "test.txt", IsDir: false, Size: int64(len(fsys["test.txt"].Data))},
|
||||
}
|
||||
|
||||
if len(expectedItems) != len(items) {
|
||||
t.Errorf("Items count mismatch, expected %d, got %d", len(expectedItems), len(items))
|
||||
return
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
if item.Name() != expectedItems[i].Name {
|
||||
t.Errorf("Item %d: expected name %s, got %s", i, expectedItems[i].Name, item.Name())
|
||||
}
|
||||
|
||||
if item.IsDir() != expectedItems[i].IsDir {
|
||||
t.Errorf("Item %d: expected IsDir %t, got %t", i, expectedItems[i].IsDir, item.IsDir())
|
||||
}
|
||||
|
||||
if item.Size() != expectedItems[i].Size {
|
||||
t.Errorf("Item %d: expected IsDir %d, got %d", i, expectedItems[i].Size, item.Size())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Readdirnames", func(t *testing.T) {
|
||||
t.Run("not directory", func(t *testing.T) {
|
||||
_, err := file.Readdirnames(-1)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("root directory", func(t *testing.T) {
|
||||
root, err := fromIOFS.Open(".")
|
||||
if err != nil {
|
||||
t.Errorf("root open failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer root.Close()
|
||||
|
||||
items, err := root.Readdirnames(-1)
|
||||
if err != nil {
|
||||
t.Errorf("Readdirnames error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var expectedItems = []string{"dir1", "dir2", "test.txt"}
|
||||
|
||||
if len(expectedItems) != len(items) {
|
||||
t.Errorf("Items count mismatch, expected %d, got %d", len(expectedItems), len(items))
|
||||
return
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
if item != expectedItems[i] {
|
||||
t.Errorf("Item %d: expected name %s, got %s", i, expectedItems[i], item)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Truncate", func(t *testing.T) {
|
||||
err := file.Truncate(1)
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
|
||||
t.Run("WriteString", func(t *testing.T) {
|
||||
_, err := file.WriteString("a")
|
||||
assertPermissionError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func assertPermissionError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
|
||||
var perr *fs.PathError
|
||||
if !errors.As(err, &perr) {
|
||||
t.Errorf("Expected *fs.PathError, got %[1]T (%[1]v)", err)
|
||||
return
|
||||
}
|
||||
|
||||
if perr.Err != fs.ErrPermission {
|
||||
t.Errorf("Expected (*fs.PathError).Err == fs.ErrPermisson, got %[1]T (%[1]v)", err)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue