Added support for rmdir, and memfs tests.
commit
e0774d1f4b
13
errors.go
13
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)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
27
server.go
27
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 {
|
||||
|
|
Loading…
Reference in New Issue