From 1569f65acadae9a6f2f35fdf029e3c9055c2a814 Mon Sep 17 00:00:00 2001 From: satotake Date: Sat, 19 Jun 2021 21:08:07 +0900 Subject: [PATCH] Add new filtering fs: PredicateFs A filtered view on predicates which takes file path as an argument, any file will be treated as non-existing when the predicate returns false. --- README.md | 21 ++++- predicatefs.go | 219 ++++++++++++++++++++++++++++++++++++++++++++ predicatefs_test.go | 65 +++++++++++++ 3 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 predicatefs.go create mode 100644 predicatefs_test.go diff --git a/README.md b/README.md index fb8eaaf..66f5747 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ _, err := fs.Create("/file.txt") // err = syscall.EPERM ``` -# RegexpFs +### RegexpFs A filtered view on file names, any file NOT matching the passed regexp will be treated as non-existing. @@ -294,7 +294,24 @@ _, err := fs.Create("/file.html") // err = syscall.ENOENT ``` -### HttpFs +### PredicateFs + +A filtered view on predicates which takes file path as an argument, +any file will be treated as non-existing when the predicate returns false. +Unlike `RegexpFs`, the fs targets not file name but file path. +Like, `RegexpFs`, files will not be created when the predicate returns false +and directories are always not filtered. + +```go +pred := func(path string) bool { + return strings.HasSuffix(path, ".txt") +} +fs := afero.NewPredicateFs(afero.NewMemMapFs(), pred) +_, err := fs.Create("/file.html") +// err = syscall.ENOENT +``` + +## HttpFs Afero provides an http compatible backend which can wrap any of the existing backends. diff --git a/predicatefs.go b/predicatefs.go new file mode 100644 index 0000000..5bef378 --- /dev/null +++ b/predicatefs.go @@ -0,0 +1,219 @@ +package afero + +import ( + "os" + "path/filepath" + "syscall" + "time" +) + +// PredicateFs filters files (not directories) by predicate, +// which takes file path as an arg. +type PredicateFs struct { + pred func(string) bool + source Fs +} + +func NewPredicateFs(source Fs, pred func(string) bool) Fs { + return &PredicateFs{source: source, pred: pred} +} + +type PredicateFile struct { + f File + pred func(string) bool +} + +func (p *PredicateFs) validate(path string) error { + if p.pred(path) { + return nil + } + return syscall.ENOENT +} + +func (p *PredicateFs) dirOrValidPath(path string) error { + dir, err := IsDir(p.source, path) + if err != nil { + return err + } + if dir { + return nil + } + return p.validate(path) +} + +func (p *PredicateFs) Chtimes(path string, a, m time.Time) error { + if err := p.dirOrValidPath(path); err != nil { + return err + } + return p.source.Chtimes(path, a, m) +} + +func (p *PredicateFs) Chmod(path string, mode os.FileMode) error { + if err := p.dirOrValidPath(path); err != nil { + return err + } + return p.source.Chmod(path, mode) +} + +func (p *PredicateFs) Chown(path string, uid, gid int) error { + if err := p.dirOrValidPath(path); err != nil { + return err + } + return p.source.Chown(path, uid, gid) +} + +func (p *PredicateFs) Name() string { + return "PredicateFs" +} + +func (p *PredicateFs) Stat(path string) (os.FileInfo, error) { + if err := p.dirOrValidPath(path); err != nil { + return nil, err + } + return p.source.Stat(path) +} + +func (p *PredicateFs) Rename(oldname, newname string) error { + dir, err := IsDir(p.source, oldname) + if err != nil { + return err + } + if dir { + return nil + } + if err := p.validate(oldname); err != nil { + return err + } + if err := p.validate(newname); err != nil { + return err + } + return p.source.Rename(oldname, newname) +} + +func (p *PredicateFs) RemoveAll(path string) error { + dir, err := IsDir(p.source, path) + if err != nil { + return err + } + if !dir { + if err := p.validate(path); err != nil { + return err + } + } + return p.source.RemoveAll(path) +} + +func (p *PredicateFs) Remove(path string) error { + if err := p.dirOrValidPath(path); err != nil { + return err + } + return p.source.Remove(path) +} + +func (p *PredicateFs) OpenFile(path string, flag int, perm os.FileMode) (File, error) { + if err := p.dirOrValidPath(path); err != nil { + return nil, err + } + return p.source.OpenFile(path, flag, perm) +} + +func (p *PredicateFs) Open(path string) (File, error) { + dir, err := IsDir(p.source, path) + if err != nil { + return nil, err + } + if !dir { + if err := p.validate(path); err != nil { + return nil, err + } + } + f, err := p.source.Open(path) + if err != nil { + return nil, err + } + return &PredicateFile{f: f, pred: p.pred}, nil +} + +func (p *PredicateFs) Mkdir(n string, path os.FileMode) error { + return p.source.Mkdir(n, path) +} + +func (p *PredicateFs) MkdirAll(n string, path os.FileMode) error { + return p.source.MkdirAll(n, path) +} + +func (p *PredicateFs) Create(path string) (File, error) { + if err := p.validate(path); err != nil { + return nil, err + } + return p.source.Create(path) +} + +func (f *PredicateFile) Close() error { + return f.f.Close() +} + +func (f *PredicateFile) Read(s []byte) (int, error) { + return f.f.Read(s) +} + +func (f *PredicateFile) ReadAt(s []byte, o int64) (int, error) { + return f.f.ReadAt(s, o) +} + +func (f *PredicateFile) Seek(o int64, w int) (int64, error) { + return f.f.Seek(o, w) +} + +func (f *PredicateFile) Write(s []byte) (int, error) { + return f.f.Write(s) +} + +func (f *PredicateFile) WriteAt(s []byte, o int64) (int, error) { + return f.f.WriteAt(s, o) +} + +func (f *PredicateFile) Name() string { + return f.f.Name() +} + +func (f *PredicateFile) Readdir(c int) (fi []os.FileInfo, err error) { + var pfi []os.FileInfo + pfi, err = f.f.Readdir(c) + if err != nil { + return nil, err + } + for _, i := range pfi { + if i.IsDir() || f.pred(filepath.Join(f.f.Name(), i.Name())) { + fi = append(fi, i) + } + } + return fi, nil +} + +func (f *PredicateFile) Readdirnames(c int) (n []string, err error) { + fi, err := f.Readdir(c) + if err != nil { + return nil, err + } + for _, s := range fi { + n = append(n, s.Name()) + } + return n, nil +} + +func (f *PredicateFile) Stat() (os.FileInfo, error) { + return f.f.Stat() +} + +func (f *PredicateFile) Sync() error { + return f.f.Sync() +} + +func (f *PredicateFile) Truncate(s int64) error { + return f.f.Truncate(s) +} + +func (f *PredicateFile) WriteString(s string) (int, error) { + return f.f.WriteString(s) +} diff --git a/predicatefs_test.go b/predicatefs_test.go new file mode 100644 index 0000000..50d61bf --- /dev/null +++ b/predicatefs_test.go @@ -0,0 +1,65 @@ +package afero + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestPredicateFs(t *testing.T) { + mfs := &MemMapFs{} + + txtExts := func(name string) bool { + return strings.HasSuffix(name, ".txt") + } + + nonEmpty := func(name string) bool { + fi, err := mfs.Stat(name) + if err != nil { + t.Errorf("Got unexpected Stat err %v", err) + } + // Note: If you use this rule, you cannot create any files on the fs + return fi.Size() > 0 + } + + inHiddenDir := func(path string) bool { + return strings.HasSuffix(filepath.Dir(path), ".hidden") + } + + pred := func(path string) bool { + return nonEmpty(path) && txtExts(path) && !inHiddenDir(path) + } + + fs := &PredicateFs{pred: pred, source: mfs} + + mfs.MkdirAll("/dir/sub/.hidden", 0777) + for _, name := range []string{"file.txt", "file.html", "empty.txt"} { + for _, dir := range []string{"/dir/", "/dir/sub/", "/dir/sub/.hidden/"} { + fh, _ := mfs.Create(dir + name) + + if !strings.HasPrefix(name, "empty") { + fh.WriteString("file content") + } + + fh.Close() + } + } + + files, _ := ReadDir(fs, "/dir") + + if len(files) != 2 { // file.txt, sub + t.Errorf("Got wrong number of files: %#v", files) + } + + f, _ := fs.Open("/dir/sub") + names, _ := f.Readdirnames(-1) + if len(names) != 2 { + // file.txt, .hidden (dirs are not filtered) + t.Errorf("Got wrong number of names: %v", names) + } + + hiddenFiles, _ := ReadDir(fs, "/dir/sub/.hidden") + if len(hiddenFiles) != 0 { + t.Errorf("Got wrong number of names: %v", hiddenFiles) + } +}