add Fs filter
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)
+}