Added support for rmdir, and memfs tests.

geesefs-0-30-9
Aaron Jacobs 2015-03-03 16:15:32 +11:00
commit e0774d1f4b
7 changed files with 324 additions and 32 deletions

View File

@ -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)
)

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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.

View File

@ -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())
}

View File

@ -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 {