diff --git a/errors.go b/errors.go index 7ed27cb..860d7ec 100644 --- a/errors.go +++ b/errors.go @@ -3,12 +3,17 @@ package fuse -import bazilfuse "bazil.org/fuse" +import ( + "syscall" + + bazilfuse "bazil.org/fuse" +) const ( // Errors corresponding to kernel error numbers. These may be treated // specially when returned by a FileSystem method. - ENOSYS = bazilfuse.ENOSYS - ENOENT = bazilfuse.ENOENT - EIO = bazilfuse.EIO + EIO = bazilfuse.EIO + ENOENT = bazilfuse.ENOENT + ENOSYS = bazilfuse.ENOSYS + ENOTEMPTY = bazilfuse.Errno(syscall.ENOTEMPTY) ) diff --git a/file_system.go b/file_system.go index cf6276c..3bbdb7b 100644 --- a/file_system.go +++ b/file_system.go @@ -46,8 +46,8 @@ type FileSystem interface { ctx context.Context, req *GetInodeAttributesRequest) (*GetInodeAttributesResponse, error) - // Forget an inode ID previously issued (e.g. by LookUpInode). The kernel - // calls this when removing an inode from its internal caches. + // Forget an inode ID previously issued (e.g. by LookUpInode or MkDir). The + // kernel calls this when removing an inode from its internal caches. ForgetInode( ctx context.Context, req *ForgetInodeRequest) (*ForgetInodeResponse, error) @@ -62,6 +62,24 @@ type FileSystem interface { ctx context.Context, req *MkDirRequest) (*MkDirResponse, error) + /////////////////////////////////// + // Inode destruction + /////////////////////////////////// + + // Unlink a directory from its parent. Because directories cannot have a link + // count above one, this means the directory inode should be deleted as well + // once the kernel calls ForgetInode. + // + // The file system is responsible for checking that the directory is empty. + // + // Sample implementation in ext2: ext2_rmdir (http://goo.gl/B9QmFf) + // + // TODO(jacobsa): Add tests for the assertion about directory link counts + // above (on a real file system and on memfs). + RmDir( + ctx context.Context, + req *RmDirRequest) (*RmDirResponse, error) + /////////////////////////////////// // Directory handles /////////////////////////////////// @@ -360,6 +378,18 @@ type MkDirResponse struct { Entry ChildInodeEntry } +type RmDirRequest struct { + Header RequestHeader + + // The ID of parent directory inode, and the name of the directory being + // removed within it. + Parent InodeID + Name string +} + +type RmDirResponse struct { +} + type OpenDirRequest struct { Header RequestHeader diff --git a/fuseutil/not_implemented_file_system.go b/fuseutil/not_implemented_file_system.go index 8453d97..e48d449 100644 --- a/fuseutil/not_implemented_file_system.go +++ b/fuseutil/not_implemented_file_system.go @@ -46,6 +46,12 @@ func (fs *NotImplementedFileSystem) MkDir( return nil, fuse.ENOSYS } +func (fs *NotImplementedFileSystem) RmDir( + ctx context.Context, + req *fuse.RmDirRequest) (*fuse.RmDirResponse, error) { + return nil, fuse.ENOSYS +} + func (fs *NotImplementedFileSystem) OpenDir( ctx context.Context, req *fuse.OpenDirRequest) (*fuse.OpenDirResponse, error) { diff --git a/samples/memfs/fs.go b/samples/memfs/fs.go index 27906ac..0a570a1 100644 --- a/samples/memfs/fs.go +++ b/samples/memfs/fs.go @@ -169,6 +169,12 @@ func (fs *memFS) allocateInode( return } +// EXCLUSIVE_LOCKS_REQUIRED(fs.mu) +func (fs *memFS) deallocateInode(id fuse.InodeID) { + fs.freeInodes = append(fs.freeInodes, id) + fs.inodes[id] = nil +} + //////////////////////////////////////////////////////////////////////// // FileSystem methods //////////////////////////////////////////////////////////////////////// @@ -294,6 +300,44 @@ func (fs *memFS) MkDir( return } +func (fs *memFS) RmDir( + ctx context.Context, + req *fuse.RmDirRequest) (resp *fuse.RmDirResponse, err error) { + resp = &fuse.RmDirResponse{} + + fs.mu.Lock() + defer fs.mu.Unlock() + + // Grab the parent, which we will update shortly. + parent := fs.getInodeForModifyingOrDie(req.Parent) + defer parent.mu.Unlock() + + // Find the child within the parent. + childID, ok := parent.LookUpChild(req.Name) + if !ok { + err = fuse.ENOENT + return + } + + // Grab the child. + child := fs.getInodeForModifyingOrDie(childID) + defer child.mu.Unlock() + + // Make sure the child is empty. + if child.Len() != 0 { + err = fuse.ENOTEMPTY + return + } + + // Remove the entry within the parent. + parent.RemoveChild(req.Name) + + // Mark the child as unlinked. + child.linkCount-- + + return +} + func (fs *memFS) OpenDir( ctx context.Context, req *fuse.OpenDirRequest) (resp *fuse.OpenDirResponse, err error) { diff --git a/samples/memfs/inode.go b/samples/memfs/inode.go index f17306e..45633b4 100644 --- a/samples/memfs/inode.go +++ b/samples/memfs/inode.go @@ -31,6 +31,13 @@ type inode struct { mu syncutil.InvariantMutex + // The number of times this inode is linked into a parent directory. This may + // be zero if the inode has been unlinked but not yet forgotten, because some + // process still has an open file handle. + // + // INVARIANT: linkCount >= 0 + linkCount int // GUARDED_BY(mu) + // The current attributes of this inode. // // INVARIANT: No non-permission mode bits are set besides os.ModeDir @@ -38,7 +45,8 @@ type inode struct { // INVARIANT: If !dir, then os.ModeDir is not set attributes fuse.InodeAttributes // GUARDED_BY(mu) - // For directories, entries describing the children of the directory. + // For directories, entries describing the children of the directory. Unused + // entries are of type DT_Unknown. // // This array can never be shortened, nor can its elements be moved, because // we use its indices for Dirent.Offset, which is exposed to the user who @@ -50,7 +58,7 @@ type inode struct { // // INVARIANT: If dir is false, this is nil. // INVARIANT: For each i, entries[i].Offset == i+1 - // INVARIANT: Contains no duplicate names. + // INVARIANT: Contains no duplicate names in used entries. entries []fuseutil.Dirent // GUARDED_BY(mu) // For files, the current contents of the file. @@ -63,8 +71,10 @@ type inode struct { // Helpers //////////////////////////////////////////////////////////////////////// +// Initially the link count is one. func newInode(attrs fuse.InodeAttributes) (in *inode) { in = &inode{ + linkCount: 1, dir: (attrs.Mode&os.ModeDir != 0), attributes: attrs, } @@ -74,6 +84,11 @@ func newInode(attrs fuse.InodeAttributes) (in *inode) { } func (inode *inode) checkInvariants() { + // Check the link count. + if inode.linkCount < 0 { + panic(fmt.Sprintf("Negative link count: %v", inode.linkCount)) + } + // No non-permission mode bits should be set besides os.ModeDir. if inode.attributes.Mode & ^(os.ModePerm|os.ModeDir) != 0 { panic(fmt.Sprintf("Unexpected mode: %v", inode.attributes.Mode)) @@ -100,11 +115,13 @@ func (inode *inode) checkInvariants() { panic(fmt.Sprintf("Unexpected offset: %v", e.Offset)) } - if _, ok := childNames[e.Name]; ok { - panic(fmt.Sprintf("Duplicate name: %s", e.Name)) - } + if e.Type != fuseutil.DT_Unknown { + if _, ok := childNames[e.Name]; ok { + panic(fmt.Sprintf("Duplicate name: %s", e.Name)) + } - childNames[e.Name] = struct{}{} + childNames[e.Name] = struct{}{} + } } } @@ -116,22 +133,18 @@ func (inode *inode) checkInvariants() { } } -//////////////////////////////////////////////////////////////////////// -// Public methods -//////////////////////////////////////////////////////////////////////// - -// Find an entry for the given child name and return its inode ID. +// Return the index of the child within inode.entries, if it exists. // // REQUIRES: inode.dir // SHARED_LOCKS_REQUIRED(inode.mu) -func (inode *inode) LookUpChild(name string) (id fuse.InodeID, ok bool) { +func (inode *inode) findChild(name string) (i int, ok bool) { if !inode.dir { - panic("LookUpChild called on non-directory.") + panic("findChild called on non-directory.") } - for _, e := range inode.entries { + var e fuseutil.Dirent + for i, e = range inode.entries { if e.Name == name { - id = e.Inode ok = true return } @@ -140,27 +153,96 @@ func (inode *inode) LookUpChild(name string) (id fuse.InodeID, ok bool) { return } +//////////////////////////////////////////////////////////////////////// +// Public methods +//////////////////////////////////////////////////////////////////////// + +// Return the number of children of the directory. +// +// REQUIRES: inode.dir +// SHARED_LOCKS_REQUIRED(inode.mu) +func (inode *inode) Len() (n int) { + for _, e := range inode.entries { + if e.Type != fuseutil.DT_Unknown { + n++ + } + } + + return +} + +// Find an entry for the given child name and return its inode ID. +// +// REQUIRES: inode.dir +// SHARED_LOCKS_REQUIRED(inode.mu) +func (inode *inode) LookUpChild(name string) (id fuse.InodeID, ok bool) { + index, ok := inode.findChild(name) + if ok { + id = inode.entries[index].Inode + } + + return +} + // Add an entry for a child. // // REQUIRES: inode.dir +// REQUIRES: dt != fuseutil.DT_Unknown // EXCLUSIVE_LOCKS_REQUIRED(inode.mu) func (inode *inode) AddChild( id fuse.InodeID, name string, dt fuseutil.DirentType) { + var index int + + // No matter where we place the entry, make sure it has the correct Offset + // field. + defer func() { + inode.entries[index].Offset = fuse.DirOffset(index + 1) + }() + + // Set up the entry. e := fuseutil.Dirent{ - Offset: fuse.DirOffset(len(inode.entries) + 1), - Inode: id, - Name: name, - Type: dt, + Inode: id, + Name: name, + Type: dt, } + // Look for a gap in which we can insert it. + for index = range inode.entries { + if inode.entries[index].Type == fuseutil.DT_Unknown { + inode.entries[index] = e + return + } + } + + // Append it to the end. + index = len(inode.entries) inode.entries = append(inode.entries, e) } +// Remove an entry for a child. +// +// REQUIRES: inode.dir +// REQUIRES: An entry for the given name exists. +// EXCLUSIVE_LOCKS_REQUIRED(inode.mu) +func (inode *inode) RemoveChild(name string) { + // Find the entry. + i, ok := inode.findChild(name) + if !ok { + panic(fmt.Sprintf("Unknown child: %s", name)) + } + + // Mark it as unused. + inode.entries[i] = fuseutil.Dirent{ + Type: fuseutil.DT_Unknown, + Offset: fuse.DirOffset(i + 1), + } +} + // Serve a ReadDir request. // -// REQUIRED: inode.dir +// REQUIRES: inode.dir // SHARED_LOCKS_REQUIRED(inode.mu) func (inode *inode) ReadDir(offset int, size int) (data []byte, err error) { if !inode.dir { @@ -168,6 +250,13 @@ func (inode *inode) ReadDir(offset int, size int) (data []byte, err error) { } for i := offset; i < len(inode.entries); i++ { + e := inode.entries[i] + + // Skip unused entries. + if e.Type == fuseutil.DT_Unknown { + continue + } + data = fuseutil.AppendDirent(data, inode.entries[i]) // Trim and stop early if we've exceeded the requested size. diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index 683413f..8428647 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -160,6 +160,7 @@ func (t *MemFSTest) Mkdir_OneLevel() { ExpectEq(0, fi.ModTime().Sub(createTime)) ExpectTrue(fi.IsDir()) + ExpectNe(0, stat.Ino) ExpectEq(1, stat.Nlink) ExpectEq(currentUid(), stat.Uid) ExpectEq(currentGid(), stat.Gid) @@ -214,6 +215,7 @@ func (t *MemFSTest) Mkdir_TwoLevels() { ExpectEq(0, fi.ModTime().Sub(createTime)) ExpectTrue(fi.IsDir()) + ExpectNe(0, stat.Ino) ExpectEq(1, stat.Nlink) ExpectEq(currentUid(), stat.Uid) ExpectEq(currentGid(), stat.Gid) @@ -324,17 +326,106 @@ func (t *MemFSTest) UnlinkFile_NonExistent() { } func (t *MemFSTest) Rmdir_NonEmpty() { - AssertTrue(false, "TODO") + var err error + + // Create two levels of directories. + err = os.MkdirAll(path.Join(t.mfs.Dir(), "foo/bar"), 0754) + AssertEq(nil, err) + + // Attempt to remove the parent. + err = os.Remove(path.Join(t.mfs.Dir(), "foo")) + + AssertNe(nil, err) + ExpectThat(err, Error(HasSubstr("not empty"))) } func (t *MemFSTest) Rmdir_Empty() { - AssertTrue(false, "TODO") -} + var err error + var entries []os.FileInfo -func (t *MemFSTest) Rmdir_NotADirectory() { - AssertTrue(false, "TODO") + // Create two levels of directories. + err = os.MkdirAll(path.Join(t.mfs.Dir(), "foo/bar"), 0754) + AssertEq(nil, err) + + // Remove the leaf. + err = os.Remove(path.Join(t.mfs.Dir(), "foo/bar")) + AssertEq(nil, err) + + // There should be nothing left in the parent. + entries, err = ioutil.ReadDir(path.Join(t.mfs.Dir(), "foo")) + + AssertEq(nil, err) + ExpectThat(entries, ElementsAre()) + + // Remove the parent. + err = os.Remove(path.Join(t.mfs.Dir(), "foo")) + AssertEq(nil, err) + + // Now the root directory should be empty, too. + entries, err = ioutil.ReadDir(t.mfs.Dir()) + + AssertEq(nil, err) + ExpectThat(entries, ElementsAre()) } func (t *MemFSTest) Rmdir_NonExistent() { - AssertTrue(false, "TODO") + err := os.Remove(path.Join(t.mfs.Dir(), "blah")) + + AssertNe(nil, err) + ExpectThat(err, Error(HasSubstr("no such file or directory"))) +} + +func (t *MemFSTest) Rmdir_OpenedForReading() { + var err error + + // Create a directory. + createTime := t.clock.Now() + err = os.Mkdir(path.Join(t.mfs.Dir(), "dir"), 0700) + AssertEq(nil, err) + + // Simulate time advancing. + t.clock.AdvanceTime(time.Second) + + // Open the directory for reading. + f, err := os.Open(path.Join(t.mfs.Dir(), "dir")) + defer func() { + if f != nil { + ExpectEq(nil, f.Close()) + } + }() + + AssertEq(nil, err) + + // Remove the directory. + err = os.Remove(path.Join(t.mfs.Dir(), "dir")) + AssertEq(nil, err) + + // Create a new directory, with the same name even, and add some contents + // within it. + err = os.MkdirAll(path.Join(t.mfs.Dir(), "dir/foo"), 0700) + AssertEq(nil, err) + + err = os.MkdirAll(path.Join(t.mfs.Dir(), "dir/bar"), 0700) + AssertEq(nil, err) + + err = os.MkdirAll(path.Join(t.mfs.Dir(), "dir/baz"), 0700) + AssertEq(nil, err) + + // We should still be able to stat the open file handle. It should show up as + // unlinked. + fi, err := f.Stat() + + ExpectEq("dir", fi.Name()) + ExpectEq(0, fi.ModTime().Sub(createTime)) + + // TODO(jacobsa): Re-enable this assertion if the following issue is fixed: + // https://github.com/bazillion/fuse/issues/66 + // ExpectEq(0, fi.Sys().(*syscall.Stat_t).Nlink) + + // Attempt to read from the directory. This should succeed even though it has + // been unlinked, and we shouldn't see any junk from the new directory. + entries, err := f.Readdir(0) + + AssertEq(nil, err) + ExpectThat(entries, ElementsAre()) } diff --git a/server.go b/server.go index 762795e..c79ff6d 100644 --- a/server.go +++ b/server.go @@ -184,6 +184,33 @@ func (s *server) handleFuseRequest(fuseReq bazilfuse.Request) { s.logger.Println("Responding:", fuseResp) typed.Respond(fuseResp) + case *bazilfuse.RemoveRequest: + // We don't yet support files. + if !typed.Dir { + s.logger.Println("Not supported for files. Returning ENOSYS.") + typed.RespondError(ENOSYS) + return + } + + // Convert the request. + req := &RmDirRequest{ + Header: convertHeader(typed.Header), + Parent: InodeID(typed.Header.Node), + Name: typed.Name, + } + + // Call the file system. + _, err := s.fs.RmDir(ctx, req) + if err != nil { + s.logger.Println("Responding:", err) + typed.RespondError(err) + return + } + + // Respond successfully. + s.logger.Println("Responding OK.") + typed.Respond() + case *bazilfuse.OpenRequest: // Directory or file? if typed.Dir {