diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..ae85b36 --- /dev/null +++ b/filter.go @@ -0,0 +1,153 @@ +package afero + +import ( + "os" + "time" +) + +// An afero Fs with an extra filter +// +// The FilterFs is run before the source Fs, any non nil error is returned +// to the caller without going to the source Fs. If every filter in the +// chain returns a nil error, the call is sent to the source Fs. +// +// see the TestReadonlyRemoveAndRead() in filter_test.go for an example use +// of filtering (e.g. admins get write access, normal users just readonly) +type FilterFs interface { + Fs + AddFilter(Fs) +} + +type Filter struct { + chain []Fs + source Fs +} + +// create a new FilterFs that implements Fs, argument must be an Fs, not +// a FilterFs +func NewFilter(fs Fs) FilterFs { + return &Filter{source: fs} +} + +// prepend a filter in the filter chain +func (f *Filter) AddFilter(fs Fs) { + c := []Fs{fs} + for _, ch := range f.chain { + c = append(c, ch) + } + f.chain = c +} + +func (f *Filter) Create(name string) (file File, err error) { + for _, c := range f.chain { + file, err = c.Create(name) + if err != nil { + return + } + } + return f.source.Create(name) +} + +func (f *Filter) Mkdir(name string, perm os.FileMode) (err error) { + for _, c := range f.chain { + err = c.Mkdir(name, perm) + if err != nil { + return + } + } + return f.source.Mkdir(name, perm) +} + +func (f *Filter) MkdirAll(path string, perm os.FileMode) (err error) { + for _, c := range f.chain { + err = c.MkdirAll(path, perm) + if err != nil { + return + } + } + return f.source.MkdirAll(path, perm) +} + +func (f *Filter) Open(name string) (file File, err error) { + for _, c := range f.chain { + file, err = c.Open(name) + if err != nil { + return + } + } + return f.source.Open(name) +} + +func (f *Filter) OpenFile(name string, flag int, perm os.FileMode) (file File, err error) { + for _, c := range f.chain { + file, err = c.OpenFile(name, flag, perm) + if err != nil { + return + } + } + return f.source.OpenFile(name, flag, perm) +} + +func (f *Filter) Remove(name string) (err error) { + for _, c := range f.chain { + err = c.Remove(name) + if err != nil { + return + } + } + return f.source.Remove(name) +} + +func (f *Filter) RemoveAll(path string) (err error) { + for _, c := range f.chain { + err = c.RemoveAll(path) + if err != nil { + return + } + } + return f.source.RemoveAll(path) +} + +func (f *Filter) Rename(oldname, newname string) (err error) { + for _, c := range f.chain { + err = c.Rename(oldname, newname) + if err != nil { + return + } + } + return f.source.Rename(oldname, newname) +} + +func (f *Filter) Stat(name string) (fi os.FileInfo, err error) { + for _, c := range f.chain { + fi, err = c.Stat(name) + if err != nil { + return + } + } + return f.source.Stat(name) +} + +func (f *Filter) Name() string { + return f.source.Name() +} + +func (f *Filter) Chmod(name string, mode os.FileMode) (err error) { + for _, c := range f.chain { + err = c.Chmod(name, mode) + if err != nil { + return + } + } + return f.source.Chmod(name, mode) +} + +func (f *Filter) Chtimes(name string, atime, mtime time.Time) (err error) { + for _, c := range f.chain { + err = c.Chtimes(name, atime, mtime) + if err != nil { + return + } + } + return f.source.Chtimes(name, atime, mtime) +} diff --git a/filter_readonly.go b/filter_readonly.go new file mode 100644 index 0000000..54672e6 --- /dev/null +++ b/filter_readonly.go @@ -0,0 +1,65 @@ +package afero + +import ( + "os" + "syscall" + "time" +) + +type ReadOnlyFilter struct { +} + +func NewReadonlyFilter() Fs { + return &ReadOnlyFilter{} +} + +func (r *ReadOnlyFilter) Chtimes(n string, a, m time.Time) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) Chmod(n string, m os.FileMode) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) Name() string { + return "readOnlyFilter" +} + +func (r *ReadOnlyFilter) Stat(name string) (os.FileInfo, error) { + return nil, nil +} + +func (r *ReadOnlyFilter) Rename(o, n string) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) RemoveAll(p string) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) Remove(n string) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + if flag&os.O_RDONLY != 0 { + return nil, nil + } + return nil, syscall.EPERM +} + +func (r *ReadOnlyFilter) Open(n string) (File, error) { + return nil, nil +} + +func (r *ReadOnlyFilter) Mkdir(n string, p os.FileMode) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) MkdirAll(n string, p os.FileMode) error { + return syscall.EPERM +} + +func (r *ReadOnlyFilter) Create(n string) (File, error) { + return nil, syscall.EPERM +} diff --git a/filter_regexp.go b/filter_regexp.go new file mode 100644 index 0000000..b061563 --- /dev/null +++ b/filter_regexp.go @@ -0,0 +1,104 @@ +package afero + +import ( + "os" + "regexp" + "syscall" + "time" +) + +type RegexpFilter struct { + file *regexp.Regexp + dir *regexp.Regexp +} + +func NewRegexpFilter(file *regexp.Regexp, dir *regexp.Regexp) Fs { + return &RegexpFilter{file: file, dir: dir} +} + +func (r *RegexpFilter) Chtimes(n string, a, m time.Time) error { + if !r.file.MatchString(n) { + return syscall.ENOENT + } + return nil +} + +func (r *RegexpFilter) Chmod(n string, m os.FileMode) error { + if !r.file.MatchString(n) { + return syscall.ENOENT + } + return nil +} + +func (r *RegexpFilter) Name() string { + return "RegexpFilter" +} + +func (r *RegexpFilter) Stat(n string) (os.FileInfo, error) { + // FIXME - what about Stat() on dirs? + if !r.file.MatchString(n) { + return nil, syscall.ENOENT + } + return nil, nil +} + +func (r *RegexpFilter) Rename(o, n string) error { + // FIXME - what about renaming dirs? + switch { + case !r.file.MatchString(o): + return syscall.ENOENT + case !r.file.MatchString(n): + return syscall.EPERM + default: + return nil + } +} + +func (r *RegexpFilter) RemoveAll(p string) error { + if !r.dir.MatchString(p) { + return syscall.EPERM // FIXME ENOENT? + } + return nil +} + +func (r *RegexpFilter) Remove(n string) error { + if !r.file.MatchString(n) { + return syscall.ENOENT + } + return nil +} + +func (r *RegexpFilter) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + if !r.file.MatchString(name) { + return nil, syscall.ENOENT + } + return nil, nil +} + +func (r *RegexpFilter) Open(n string) (File, error) { + if !r.file.MatchString(n) { + return nil, syscall.ENOENT + } + return nil, nil +} + +func (r *RegexpFilter) Mkdir(n string, p os.FileMode) error { + if !r.dir.MatchString(n) { + return syscall.EPERM + } + return nil +} + +func (r *RegexpFilter) MkdirAll(n string, p os.FileMode) error { + if !r.dir.MatchString(n) { + return syscall.EPERM + } + return nil +} + +func (r *RegexpFilter) Create(n string) (File, error) { + if !r.file.MatchString(n) { + return nil, syscall.EPERM + } + return nil, nil +} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..d05a707 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,77 @@ +package afero + +import ( + "regexp" + "testing" +) + +func TestReadOnly(t *testing.T) { + mfs := &MemMapFs{} + fs := NewFilter(mfs) + fs.AddFilter(NewReadonlyFilter()) + _, err := fs.Create("/file.txt") + if err == nil { + t.Errorf("Did not fail to create file") + } + t.Logf("ERR=%s", err) +} + +func TestReadonlyRemoveAndRead(t *testing.T) { + mfs := &MemMapFs{} + fh, err := mfs.Create("/file.txt") + fh.Write([]byte("content here")) + fh.Close() + + fs := NewFilter(mfs) + fs.AddFilter(NewReadonlyFilter()) + err = fs.Remove("/file.txt") + if err == nil { + t.Errorf("Did not fail to remove file") + } + + fh, err = fs.Open("/file.txt") + if err != nil { + t.Errorf("Failed to open file: %s", err) + } + + buf := make([]byte, len("content here")) + _, err = fh.Read(buf) + fh.Close() + if string(buf) != "content here" { + t.Errorf("Failed to read file: %s", err) + } + + err = mfs.Remove("/file.txt") + if err != nil { + t.Errorf("Failed to remove file") + } + + fh, err = fs.Open("/file.txt") + if err == nil { + fh.Close() + t.Errorf("File still present") + } +} + +func TestRegexp(t *testing.T) { + mfs := &MemMapFs{} + fs := NewFilter(mfs) + fs.AddFilter(NewRegexpFilter(regexp.MustCompile(`\.txt$`), nil)) + _, err := fs.Create("/file.html") + if err == nil { + t.Errorf("Did not fail to create file") + } + t.Logf("ERR=%s", err) +} + +func TestRORegexpChain(t *testing.T) { + mfs := &MemMapFs{} + fs := NewFilter(mfs) + fs.AddFilter(NewReadonlyFilter()) + fs.AddFilter(NewRegexpFilter(regexp.MustCompile(`\.txt$`), nil)) + _, err := fs.Create("/file.txt") + if err == nil { + t.Errorf("Did not fail to create file") + } + t.Logf("ERR=%s", err) +}