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.
This commit is contained in:
satotake 2021-06-19 21:08:07 +09:00
parent bc94f58bed
commit 1569f65aca
3 changed files with 303 additions and 2 deletions

View File

@ -281,7 +281,7 @@ _, err := fs.Create("/file.txt")
// err = syscall.EPERM // err = syscall.EPERM
``` ```
# RegexpFs ### RegexpFs
A filtered view on file names, any file NOT matching A filtered view on file names, any file NOT matching
the passed regexp will be treated as non-existing. the passed regexp will be treated as non-existing.
@ -294,7 +294,24 @@ _, err := fs.Create("/file.html")
// err = syscall.ENOENT // 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 Afero provides an http compatible backend which can wrap any of the existing
backends. backends.

219
predicatefs.go Normal file
View File

@ -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)
}

65
predicatefs_test.go Normal file
View File

@ -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)
}
}