Skip to content

Commit e815127

Browse files
evankandersonpjbgf
authored andcommitted
Add wrapper for io/fs
Signed-off-by: Evan Anderson <[email protected]>
1 parent dc481f5 commit e815127

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed

helper/iofs/iofs.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Package iofs provides an adapter from billy.Filesystem to a the
2+
// standard library io.fs.FS interface.
3+
package iofs
4+
5+
import (
6+
"io"
7+
"io/fs"
8+
"path/filepath"
9+
10+
billyfs "github.com/go-git/go-billy/v5"
11+
"github.com/go-git/go-billy/v5/helper/polyfill"
12+
)
13+
14+
// Wrap adapts a billy.Filesystem to a io.fs.FS.
15+
func Wrap(fs billyfs.Basic) fs.FS {
16+
return &adapterFs{fs: polyfill.New(fs)}
17+
}
18+
19+
type adapterFs struct {
20+
fs billyfs.Filesystem
21+
}
22+
23+
var _ fs.FS = (*adapterFs)(nil)
24+
var _ fs.ReadDirFS = (*adapterFs)(nil)
25+
var _ fs.StatFS = (*adapterFs)(nil)
26+
var _ fs.ReadFileFS = (*adapterFs)(nil)
27+
28+
// GlobFS would be harder, we don't implement for now.
29+
30+
// Open implements fs.FS.
31+
func (a *adapterFs) Open(name string) (fs.File, error) {
32+
if name[0] == '/' || name != filepath.Clean(name) {
33+
// fstest.TestFS explicitly checks that these should return error
34+
// MemFS is performs the clean internally, so we need to block that here for testing.
35+
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
36+
}
37+
stat, err := a.fs.Stat(name)
38+
if err != nil {
39+
return nil, err
40+
}
41+
if stat.IsDir() {
42+
entries, err := a.ReadDir(name)
43+
if err != nil {
44+
return nil, err
45+
}
46+
return makeDir(stat, entries), nil
47+
}
48+
file, err := a.fs.Open(name)
49+
return &adapterFile{file: file, info: stat}, err
50+
}
51+
52+
// ReadDir implements fs.ReadDirFS.
53+
func (a *adapterFs) ReadDir(name string) ([]fs.DirEntry, error) {
54+
items, err := a.fs.ReadDir(name)
55+
if err != nil {
56+
return nil, err
57+
}
58+
entries := make([]fs.DirEntry, len(items))
59+
for i, item := range items {
60+
entries[i] = fs.FileInfoToDirEntry(item)
61+
}
62+
return entries, nil
63+
}
64+
65+
// Stat implements fs.StatFS.
66+
func (a *adapterFs) Stat(name string) (fs.FileInfo, error) {
67+
return a.fs.Stat(name)
68+
}
69+
70+
// ReadFile implements fs.ReadFileFS.
71+
func (a *adapterFs) ReadFile(name string) ([]byte, error) {
72+
stat, err := a.fs.Stat(name)
73+
if err != nil {
74+
return nil, err
75+
}
76+
b := make([]byte, stat.Size())
77+
file, err := a.Open(name)
78+
if err != nil {
79+
return nil, err
80+
}
81+
defer file.Close()
82+
_, err = file.Read(b)
83+
return b, err
84+
}
85+
86+
type adapterFile struct {
87+
file billyfs.File
88+
info fs.FileInfo
89+
}
90+
91+
var _ fs.File = (*adapterFile)(nil)
92+
93+
// Close implements fs.File.
94+
func (a *adapterFile) Close() error {
95+
return a.file.Close()
96+
}
97+
98+
// Read implements fs.File.
99+
func (a *adapterFile) Read(b []byte) (int, error) {
100+
return a.file.Read(b)
101+
}
102+
103+
// Stat implements fs.File.
104+
func (a *adapterFile) Stat() (fs.FileInfo, error) {
105+
return a.info, nil
106+
}
107+
108+
type adapterDirFile struct {
109+
adapterFile
110+
entries []fs.DirEntry
111+
}
112+
113+
var _ fs.ReadDirFile = (*adapterDirFile)(nil)
114+
115+
func makeDir(stat fs.FileInfo, entries []fs.DirEntry) *adapterDirFile {
116+
return &adapterDirFile{
117+
adapterFile: adapterFile{info: stat},
118+
entries: entries,
119+
}
120+
}
121+
122+
// Close implements fs.File.
123+
// Subtle: note that this is shadowing adapterFile.Close.
124+
func (a *adapterDirFile) Close() error {
125+
return nil
126+
}
127+
128+
// ReadDir implements fs.ReadDirFile.
129+
func (a *adapterDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
130+
if len(a.entries) == 0 && n > 0 {
131+
return nil, io.EOF
132+
}
133+
if n <= 0 {
134+
n = len(a.entries)
135+
}
136+
if n > len(a.entries) {
137+
n = len(a.entries)
138+
}
139+
entries := a.entries[:n]
140+
a.entries = a.entries[n:]
141+
return entries, nil
142+
}

helper/iofs/iofs_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package iofs
2+
3+
import (
4+
"errors"
5+
"io/fs"
6+
"strings"
7+
"testing"
8+
"testing/fstest"
9+
10+
billyfs "github.com/go-git/go-billy/v5"
11+
"github.com/go-git/go-billy/v5/memfs"
12+
)
13+
14+
type errorList interface {
15+
Unwrap() []error
16+
}
17+
18+
type wrappedError interface {
19+
Unwrap() error
20+
}
21+
22+
// TestWithFSTest leverages the packaged Go fstest package, which seems comprehensive
23+
func TestWithFSTest(t *testing.T) {
24+
t.Parallel()
25+
memfs := memfs.New()
26+
iofs := Wrap(memfs)
27+
28+
files := map[string]string{
29+
"foo.txt": "hello, world",
30+
"bar.txt": "goodbye, world",
31+
"dir/baz.txt": "こんにちわ, world",
32+
}
33+
created_files := make([]string, 0, len(files))
34+
for filename, contents := range files {
35+
makeFile(memfs, t, filename, contents)
36+
created_files = append(created_files, filename)
37+
}
38+
39+
err := fstest.TestFS(iofs, created_files...)
40+
if err != nil {
41+
if unwrapped := errors.Unwrap(err); unwrapped != nil {
42+
err = unwrapped
43+
}
44+
if errs, ok := err.(errorList); ok {
45+
for _, e := range errs.Unwrap() {
46+
47+
if strings.Contains(e.Error(), "ModTime") {
48+
// Memfs returns the current time for Stat().ModTime(), which triggers
49+
// a diff complaint in fstest. We can ignore this, or store modtimes
50+
// for every file in Memfs (at a cost of 16 bytes / file).
51+
t.Log("Skipping ModTime error (ok).")
52+
} else {
53+
t.Errorf("Unexpected fstest error: %v", e)
54+
}
55+
}
56+
} else {
57+
t.Fatalf("Failed to test fs:\n%v", err)
58+
}
59+
}
60+
}
61+
62+
func TestDeletes(t *testing.T) {
63+
t.Parallel()
64+
memfs := memfs.New()
65+
iofs := Wrap(memfs).(fs.ReadFileFS)
66+
67+
makeFile(memfs, t, "foo.txt", "hello, world")
68+
makeFile(memfs, t, "deleted", "nothing to see")
69+
70+
if _, err := iofs.ReadFile("nonexistent"); err == nil {
71+
t.Errorf("expected error for nonexistent file")
72+
}
73+
74+
data, err := iofs.ReadFile("deleted")
75+
if err != nil {
76+
t.Fatalf("failed to read file before delete: %v", err)
77+
}
78+
if string(data) != "nothing to see" {
79+
t.Errorf("unexpected contents before delete: %v", data)
80+
}
81+
82+
if err := memfs.Remove("deleted"); err != nil {
83+
t.Fatalf("failed to remove file: %v", err)
84+
}
85+
86+
if _, err = iofs.ReadFile("deleted"); err == nil {
87+
t.Errorf("file existed after delete!")
88+
}
89+
}
90+
91+
func makeFile(fs billyfs.Basic, t *testing.T, filename string, contents string) {
92+
t.Helper()
93+
file, err := fs.Create(filename)
94+
if err != nil {
95+
t.Fatalf("failed to create file %s: %v", filename, err)
96+
}
97+
defer file.Close()
98+
_, err = file.Write([]byte(contents))
99+
if err != nil {
100+
t.Fatalf("failed to write to file %s: %v", filename, err)
101+
}
102+
}

0 commit comments

Comments
 (0)