diff --git a/fs.go b/fs.go index 235e98f..042d3d7 100644 --- a/fs.go +++ b/fs.go @@ -65,3 +65,80 @@ func (f *BaseFile) Filename() string { func (f *BaseFile) IsClosed() bool { return f.Closed } + +type removerAll interface { + RemoveAll(string) error +} + +// RemoveAll removes path and any children it contains. +// It removes everything it can but returns the first error +// it encounters. If the path does not exist, RemoveAll +// returns nil (no error). +func RemoveAll(fs Filesystem, path string) error { + r, ok := fs.(removerAll) + if ok { + return r.RemoveAll(path) + } + + return removeAll(fs, path) +} + +func removeAll(fs Filesystem, path string) error { + // This implementation is adapted from os.RemoveAll. + + // Simple case: if Remove works, we're done. + err := fs.Remove(path) + if err == nil || os.IsNotExist(err) { + return nil + } + + // Otherwise, is this a directory we need to recurse into? + dir, serr := fs.Stat(path) + if serr != nil { + if os.IsNotExist(serr) { + return nil + } + + return serr + } + + if !dir.IsDir() { + // Not a directory; return the error from Remove. + return err + } + + // Directory. + fis, err := fs.ReadDir(path) + if err != nil { + if os.IsNotExist(err) { + // Race. It was deleted between the Lstat and Open. + // Return nil per RemoveAll's docs. + return nil + } + + return err + } + + // Remove contents & return first error. + err = nil + for _, fi := range fis { + cpath := fs.Join(path, fi.Name()) + err1 := removeAll(fs, cpath) + if err == nil { + err = err1 + } + } + + // Remove directory. + err1 := fs.Remove(path) + if err1 == nil || os.IsNotExist(err1) { + return nil + } + + if err == nil { + err = err1 + } + + return err + +} diff --git a/memfs/memory.go b/memfs/memory.go index fbd2afc..dcafd71 100644 --- a/memfs/memory.go +++ b/memfs/memory.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "strings" "time" @@ -78,7 +79,9 @@ func (fs *Memory) Stat(filename string) (billy.FileInfo, error) { info, err := fs.ReadDir(fullpath) if err == nil && len(info) != 0 { - return newFileInfo(fs.base, fullpath, len(info)+100), nil + fi := newFileInfo(fs.base, fullpath, len(info)) + fi.isDir = true + return fi, nil } return nil, os.ErrNotExist @@ -90,7 +93,7 @@ func (fs *Memory) ReadDir(base string) (entries []billy.FileInfo, err error) { appendedDirs := make(map[string]bool, 0) for fullpath, f := range fs.s.files { - if !strings.HasPrefix(fullpath, base) { + if !isInDir(base, fullpath) { continue } @@ -158,6 +161,10 @@ func (fs *Memory) Rename(from, to string) error { func (fs *Memory) Remove(filename string) error { fullpath := fs.Join(fs.base, filename) if _, ok := fs.s.files[fullpath]; !ok { + if fs.isDir(fullpath) { + return fmt.Errorf("directory not empty: %s", filename) + } + return os.ErrNotExist } @@ -184,6 +191,16 @@ func (fs *Memory) Base() string { return fs.base } +func (fs *Memory) isDir(path string) bool { + for fpath := range fs.s.files { + if isInDir(path, fpath) { + return true + } + } + + return false +} + type file struct { billy.BaseFile @@ -375,3 +392,19 @@ func isReadOnly(flag int) bool { func isWriteOnly(flag int) bool { return flag&os.O_WRONLY != 0 } + +func isInDir(dir, other string) bool { + dir = path.Clean(dir) + dir = toTrailingSlash(dir) + other = path.Clean(other) + + return strings.HasPrefix(other, dir) +} + +func toTrailingSlash(p string) string { + if strings.HasSuffix(p, "/") { + return p + } + + return p + "/" +} diff --git a/osfs/os.go b/osfs/os.go index 79156bc..55bd638 100644 --- a/osfs/os.go +++ b/osfs/os.go @@ -151,6 +151,13 @@ func (fs *OS) Base() string { return fs.base } +// RemoveAll removes a file or directory recursively. Removes everything it can, +// but returns the first error. +func (fs *OS) RemoveAll(path string) error { + fullpath := fs.Join(fs.base, path) + return os.RemoveAll(fullpath) +} + // osFile represents a file in the os filesystem type osFile struct { billy.BaseFile diff --git a/test/fs_suite.go b/test/fs_suite.go index cf03938..d799ba7 100644 --- a/test/fs_suite.go +++ b/test/fs_suite.go @@ -299,6 +299,13 @@ func (s *FilesystemSuite) TestReadDirFileInfoDirs(c *C) { c.Assert(info[0].Name(), Equals, "foo") } +func (s *FilesystemSuite) TestStatNonExistent(c *C) { + fi, err := s.Fs.Stat("non-existent") + comment := Commentf("error: %s", err) + c.Assert(os.IsNotExist(err), Equals, true, comment) + c.Assert(fi, IsNil) +} + func (s *FilesystemSuite) TestDirStat(c *C) { files := []string{"foo", "bar", "qux/baz", "qux/qux"} for _, name := range files { @@ -307,14 +314,28 @@ func (s *FilesystemSuite) TestDirStat(c *C) { c.Assert(f.Close(), IsNil) } + // Some implementations detect directories based on a prefix + // for all files; it's easy to miss path separator handling there. + fi, err := s.Fs.Stat("qu") + c.Assert(os.IsNotExist(err), Equals, true, Commentf("error: %s", err)) + c.Assert(fi, IsNil) + + fi, err = s.Fs.Stat("qux") + c.Assert(err, IsNil) + c.Assert(fi.Name(), Equals, "qux") + c.Assert(fi.IsDir(), Equals, true) + qux := s.Fs.Dir("qux") - fi, err := qux.Stat("baz") + + fi, err = qux.Stat("baz") c.Assert(err, IsNil) c.Assert(fi.Name(), Equals, "baz") + c.Assert(fi.IsDir(), Equals, false) fi, err = qux.Stat("/baz") c.Assert(err, IsNil) c.Assert(fi.Name(), Equals, "baz") + c.Assert(fi.IsDir(), Equals, false) } func (s *FilesystemSuite) TestCreateInDir(c *C) { @@ -335,7 +356,7 @@ func (s *FilesystemSuite) TestRename(c *C) { foo, err := s.Fs.Stat("foo") c.Assert(foo, IsNil) - c.Assert(err, NotNil) + c.Assert(os.IsNotExist(err), Equals, true) bar, err := s.Fs.Stat("bar") c.Assert(bar, NotNil) @@ -404,7 +425,18 @@ func (s *FilesystemSuite) TestRemove(c *C) { } func (s *FilesystemSuite) TestRemoveNonExisting(c *C) { - c.Assert(s.Fs.Remove("NON-EXISTING"), NotNil) + err := s.Fs.Remove("NON-EXISTING") + c.Assert(err, NotNil) + c.Assert(os.IsNotExist(err), Equals, true) +} + +func (s *FilesystemSuite) TestRemoveNotEmptyDir(c *C) { + f, err := s.Fs.Create("foo/bar") + c.Assert(err, IsNil) + c.Assert(f.Close(), IsNil) + + err = s.Fs.Remove("foo") + c.Assert(err, NotNil) } func (s *FilesystemSuite) TestRemoveTempFile(c *C) { @@ -479,3 +511,61 @@ func (s *FilesystemSuite) TestReadWriteLargeFile(c *C) { c.Assert(err, IsNil) c.Assert(len(b), Equals, size) } + +func (s *FilesystemSuite) TestRemoveAllNonExistent(c *C) { + c.Assert(RemoveAll(s.Fs, "non-existent"), IsNil) +} + +func (s *FilesystemSuite) TestRemoveAll(c *C) { + fnames := []string{ + "foo/1", + "foo/2", + "foo/bar/1", + "foo/bar/2", + "foo/bar/baz/1", + "foo/bar/baz/qux/1", + "foo/bar/baz/qux/2", + "foo/bar/baz/qux/3", + } + + for _, fname := range fnames { + f, err := s.Fs.Create(fname) + c.Assert(err, IsNil) + c.Assert(f.Close(), IsNil) + } + + c.Assert(RemoveAll(s.Fs, "foo"), IsNil) + + for _, fname := range fnames { + _, err := s.Fs.Stat(fname) + comment := Commentf("not removed: %s %s", fname, err) + c.Assert(os.IsNotExist(err), Equals, true, comment) + } +} + +func (s *FilesystemSuite) TestRemoveAllRelative(c *C) { + fnames := []string{ + "foo/1", + "foo/2", + "foo/bar/1", + "foo/bar/2", + "foo/bar/baz/1", + "foo/bar/baz/qux/1", + "foo/bar/baz/qux/2", + "foo/bar/baz/qux/3", + } + + for _, fname := range fnames { + f, err := s.Fs.Create(fname) + c.Assert(err, IsNil) + c.Assert(f.Close(), IsNil) + } + + c.Assert(RemoveAll(s.Fs, "foo/bar/.."), IsNil) + + for _, fname := range fnames { + _, err := s.Fs.Stat(fname) + comment := Commentf("not removed: %s %s", fname, err) + c.Assert(os.IsNotExist(err), Equals, true, comment) + } +}