Skip to content

Commit 9c1722d

Browse files
authored
Merge pull request #12 from smola/removeall
add RemoveAll
2 parents f84fa56 + 3d9b579 commit 9c1722d

File tree

4 files changed

+212
-5
lines changed

4 files changed

+212
-5
lines changed

fs.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,80 @@ func (f *BaseFile) Filename() string {
6565
func (f *BaseFile) IsClosed() bool {
6666
return f.Closed
6767
}
68+
69+
type removerAll interface {
70+
RemoveAll(string) error
71+
}
72+
73+
// RemoveAll removes path and any children it contains.
74+
// It removes everything it can but returns the first error
75+
// it encounters. If the path does not exist, RemoveAll
76+
// returns nil (no error).
77+
func RemoveAll(fs Filesystem, path string) error {
78+
r, ok := fs.(removerAll)
79+
if ok {
80+
return r.RemoveAll(path)
81+
}
82+
83+
return removeAll(fs, path)
84+
}
85+
86+
func removeAll(fs Filesystem, path string) error {
87+
// This implementation is adapted from os.RemoveAll.
88+
89+
// Simple case: if Remove works, we're done.
90+
err := fs.Remove(path)
91+
if err == nil || os.IsNotExist(err) {
92+
return nil
93+
}
94+
95+
// Otherwise, is this a directory we need to recurse into?
96+
dir, serr := fs.Stat(path)
97+
if serr != nil {
98+
if os.IsNotExist(serr) {
99+
return nil
100+
}
101+
102+
return serr
103+
}
104+
105+
if !dir.IsDir() {
106+
// Not a directory; return the error from Remove.
107+
return err
108+
}
109+
110+
// Directory.
111+
fis, err := fs.ReadDir(path)
112+
if err != nil {
113+
if os.IsNotExist(err) {
114+
// Race. It was deleted between the Lstat and Open.
115+
// Return nil per RemoveAll's docs.
116+
return nil
117+
}
118+
119+
return err
120+
}
121+
122+
// Remove contents & return first error.
123+
err = nil
124+
for _, fi := range fis {
125+
cpath := fs.Join(path, fi.Name())
126+
err1 := removeAll(fs, cpath)
127+
if err == nil {
128+
err = err1
129+
}
130+
}
131+
132+
// Remove directory.
133+
err1 := fs.Remove(path)
134+
if err1 == nil || os.IsNotExist(err1) {
135+
return nil
136+
}
137+
138+
if err == nil {
139+
err = err1
140+
}
141+
142+
return err
143+
144+
}

memfs/memory.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"os"
9+
"path"
910
"path/filepath"
1011
"strings"
1112
"time"
@@ -78,7 +79,9 @@ func (fs *Memory) Stat(filename string) (billy.FileInfo, error) {
7879

7980
info, err := fs.ReadDir(fullpath)
8081
if err == nil && len(info) != 0 {
81-
return newFileInfo(fs.base, fullpath, len(info)+100), nil
82+
fi := newFileInfo(fs.base, fullpath, len(info))
83+
fi.isDir = true
84+
return fi, nil
8285
}
8386

8487
return nil, os.ErrNotExist
@@ -90,7 +93,7 @@ func (fs *Memory) ReadDir(base string) (entries []billy.FileInfo, err error) {
9093

9194
appendedDirs := make(map[string]bool, 0)
9295
for fullpath, f := range fs.s.files {
93-
if !strings.HasPrefix(fullpath, base) {
96+
if !isInDir(base, fullpath) {
9497
continue
9598
}
9699

@@ -158,6 +161,10 @@ func (fs *Memory) Rename(from, to string) error {
158161
func (fs *Memory) Remove(filename string) error {
159162
fullpath := fs.Join(fs.base, filename)
160163
if _, ok := fs.s.files[fullpath]; !ok {
164+
if fs.isDir(fullpath) {
165+
return fmt.Errorf("directory not empty: %s", filename)
166+
}
167+
161168
return os.ErrNotExist
162169
}
163170

@@ -184,6 +191,16 @@ func (fs *Memory) Base() string {
184191
return fs.base
185192
}
186193

194+
func (fs *Memory) isDir(path string) bool {
195+
for fpath := range fs.s.files {
196+
if isInDir(path, fpath) {
197+
return true
198+
}
199+
}
200+
201+
return false
202+
}
203+
187204
type file struct {
188205
billy.BaseFile
189206

@@ -375,3 +392,19 @@ func isReadOnly(flag int) bool {
375392
func isWriteOnly(flag int) bool {
376393
return flag&os.O_WRONLY != 0
377394
}
395+
396+
func isInDir(dir, other string) bool {
397+
dir = path.Clean(dir)
398+
dir = toTrailingSlash(dir)
399+
other = path.Clean(other)
400+
401+
return strings.HasPrefix(other, dir)
402+
}
403+
404+
func toTrailingSlash(p string) string {
405+
if strings.HasSuffix(p, "/") {
406+
return p
407+
}
408+
409+
return p + "/"
410+
}

osfs/os.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ func (fs *OS) Base() string {
151151
return fs.base
152152
}
153153

154+
// RemoveAll removes a file or directory recursively. Removes everything it can,
155+
// but returns the first error.
156+
func (fs *OS) RemoveAll(path string) error {
157+
fullpath := fs.Join(fs.base, path)
158+
return os.RemoveAll(fullpath)
159+
}
160+
154161
// osFile represents a file in the os filesystem
155162
type osFile struct {
156163
billy.BaseFile

test/fs_suite.go

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,13 @@ func (s *FilesystemSuite) TestReadDirFileInfoDirs(c *C) {
299299
c.Assert(info[0].Name(), Equals, "foo")
300300
}
301301

302+
func (s *FilesystemSuite) TestStatNonExistent(c *C) {
303+
fi, err := s.Fs.Stat("non-existent")
304+
comment := Commentf("error: %s", err)
305+
c.Assert(os.IsNotExist(err), Equals, true, comment)
306+
c.Assert(fi, IsNil)
307+
}
308+
302309
func (s *FilesystemSuite) TestDirStat(c *C) {
303310
files := []string{"foo", "bar", "qux/baz", "qux/qux"}
304311
for _, name := range files {
@@ -307,14 +314,28 @@ func (s *FilesystemSuite) TestDirStat(c *C) {
307314
c.Assert(f.Close(), IsNil)
308315
}
309316

317+
// Some implementations detect directories based on a prefix
318+
// for all files; it's easy to miss path separator handling there.
319+
fi, err := s.Fs.Stat("qu")
320+
c.Assert(os.IsNotExist(err), Equals, true, Commentf("error: %s", err))
321+
c.Assert(fi, IsNil)
322+
323+
fi, err = s.Fs.Stat("qux")
324+
c.Assert(err, IsNil)
325+
c.Assert(fi.Name(), Equals, "qux")
326+
c.Assert(fi.IsDir(), Equals, true)
327+
310328
qux := s.Fs.Dir("qux")
311-
fi, err := qux.Stat("baz")
329+
330+
fi, err = qux.Stat("baz")
312331
c.Assert(err, IsNil)
313332
c.Assert(fi.Name(), Equals, "baz")
333+
c.Assert(fi.IsDir(), Equals, false)
314334

315335
fi, err = qux.Stat("/baz")
316336
c.Assert(err, IsNil)
317337
c.Assert(fi.Name(), Equals, "baz")
338+
c.Assert(fi.IsDir(), Equals, false)
318339
}
319340

320341
func (s *FilesystemSuite) TestCreateInDir(c *C) {
@@ -335,7 +356,7 @@ func (s *FilesystemSuite) TestRename(c *C) {
335356

336357
foo, err := s.Fs.Stat("foo")
337358
c.Assert(foo, IsNil)
338-
c.Assert(err, NotNil)
359+
c.Assert(os.IsNotExist(err), Equals, true)
339360

340361
bar, err := s.Fs.Stat("bar")
341362
c.Assert(bar, NotNil)
@@ -404,7 +425,18 @@ func (s *FilesystemSuite) TestRemove(c *C) {
404425
}
405426

406427
func (s *FilesystemSuite) TestRemoveNonExisting(c *C) {
407-
c.Assert(s.Fs.Remove("NON-EXISTING"), NotNil)
428+
err := s.Fs.Remove("NON-EXISTING")
429+
c.Assert(err, NotNil)
430+
c.Assert(os.IsNotExist(err), Equals, true)
431+
}
432+
433+
func (s *FilesystemSuite) TestRemoveNotEmptyDir(c *C) {
434+
f, err := s.Fs.Create("foo/bar")
435+
c.Assert(err, IsNil)
436+
c.Assert(f.Close(), IsNil)
437+
438+
err = s.Fs.Remove("foo")
439+
c.Assert(err, NotNil)
408440
}
409441

410442
func (s *FilesystemSuite) TestRemoveTempFile(c *C) {
@@ -479,3 +511,61 @@ func (s *FilesystemSuite) TestReadWriteLargeFile(c *C) {
479511
c.Assert(err, IsNil)
480512
c.Assert(len(b), Equals, size)
481513
}
514+
515+
func (s *FilesystemSuite) TestRemoveAllNonExistent(c *C) {
516+
c.Assert(RemoveAll(s.Fs, "non-existent"), IsNil)
517+
}
518+
519+
func (s *FilesystemSuite) TestRemoveAll(c *C) {
520+
fnames := []string{
521+
"foo/1",
522+
"foo/2",
523+
"foo/bar/1",
524+
"foo/bar/2",
525+
"foo/bar/baz/1",
526+
"foo/bar/baz/qux/1",
527+
"foo/bar/baz/qux/2",
528+
"foo/bar/baz/qux/3",
529+
}
530+
531+
for _, fname := range fnames {
532+
f, err := s.Fs.Create(fname)
533+
c.Assert(err, IsNil)
534+
c.Assert(f.Close(), IsNil)
535+
}
536+
537+
c.Assert(RemoveAll(s.Fs, "foo"), IsNil)
538+
539+
for _, fname := range fnames {
540+
_, err := s.Fs.Stat(fname)
541+
comment := Commentf("not removed: %s %s", fname, err)
542+
c.Assert(os.IsNotExist(err), Equals, true, comment)
543+
}
544+
}
545+
546+
func (s *FilesystemSuite) TestRemoveAllRelative(c *C) {
547+
fnames := []string{
548+
"foo/1",
549+
"foo/2",
550+
"foo/bar/1",
551+
"foo/bar/2",
552+
"foo/bar/baz/1",
553+
"foo/bar/baz/qux/1",
554+
"foo/bar/baz/qux/2",
555+
"foo/bar/baz/qux/3",
556+
}
557+
558+
for _, fname := range fnames {
559+
f, err := s.Fs.Create(fname)
560+
c.Assert(err, IsNil)
561+
c.Assert(f.Close(), IsNil)
562+
}
563+
564+
c.Assert(RemoveAll(s.Fs, "foo/bar/.."), IsNil)
565+
566+
for _, fname := range fnames {
567+
_, err := s.Fs.Stat(fname)
568+
comment := Commentf("not removed: %s %s", fname, err)
569+
c.Assert(os.IsNotExist(err), Equals, true, comment)
570+
}
571+
}

0 commit comments

Comments
 (0)