Merge pull request #36 from sbg/feature/hard-links

Hard links implementation
geesefs-0-30-9
Aaron Jacobs 2017-12-01 13:26:32 +11:00 committed by GitHub
commit c4e473376f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 375 additions and 20 deletions

View File

@ -420,6 +420,32 @@ func convertInMessage(
Flags: fusekernel.InitFlags(in.Flags),
}
case fusekernel.OpLink:
type input fusekernel.LinkIn
in := (*input)(inMsg.Consume(unsafe.Sizeof(input{})))
if in == nil {
err = errors.New("Corrupt OpLink")
return
}
name := inMsg.ConsumeBytes(inMsg.Len())
i := bytes.IndexByte(name, '\x00')
if i < 0 {
err = errors.New("Corrupt OpLink")
return
}
name = name[:i]
if len(name) == 0 {
err = errors.New("Corrupt OpLink (Name not read)")
return
}
o = &fuseops.CreateLinkOp{
Parent: fuseops.InodeID(inMsg.Header().Nodeid),
Name: string(name),
Target: fuseops.InodeID(in.Oldnodeid),
}
case fusekernel.OpRemovexattr:
buf := inMsg.ConsumeBytes(inMsg.Len())
n := len(buf)
@ -647,6 +673,11 @@ func (c *Connection) kernelResponseForOp(
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
case *fuseops.CreateLinkOp:
size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
case *fuseops.RenameOp:
// Empty response

View File

@ -317,6 +317,26 @@ type CreateSymlinkOp struct {
Entry ChildInodeEntry
}
// Create a hard link to an inode. If the name already exists, the file system
// should return EEXIST (cf. the notes on CreateFileOp and MkDirOp).
type CreateLinkOp struct {
// The ID of parent directory inode within which to create the child hard
// link.
Parent InodeID
// The name of the new inode.
Name string
// The ID of the target inode.
Target InodeID
// Set by the file system: information about the inode that was created.
//
// The lookup count for the inode is implicitly incremented. See notes on
// ForgetInodeOp for more information.
Entry ChildInodeEntry
}
////////////////////////////////////////////////////////////////////////
// Unlinking
////////////////////////////////////////////////////////////////////////

View File

@ -365,3 +365,71 @@ func RunSymlinkInParallelTest(
AssertEq(nil, err)
}
}
// Run an ogletest test that checks expectations for parallel calls to
// link(2).
func RunHardlinkInParallelTest(
ctx context.Context,
dir string) {
// Ensure that we get parallelism for this test.
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(runtime.NumCPU()))
// Create a file.
originalFile := path.Join(dir, "original_file")
const contents = "Hello\x00world"
err := ioutil.WriteFile(originalFile, []byte(contents), 0444)
AssertEq(nil, err)
// Try for awhile to see if anything breaks.
const duration = 500 * time.Millisecond
startTime := time.Now()
for time.Since(startTime) < duration {
filename := path.Join(dir, "foo")
// Set up a function that creates the symlink, ignoring EEXIST errors.
worker := func(id byte) (err error) {
err = os.Link(originalFile, filename)
if os.IsExist(err) {
err = nil
}
if err != nil {
err = fmt.Errorf("Worker %d: Link: %v", id, err)
return
}
return
}
// Run several workers in parallel.
const numWorkers = 16
b := syncutil.NewBundle(ctx)
for i := 0; i < numWorkers; i++ {
id := byte(i)
b.Add(func(ctx context.Context) (err error) {
err = worker(id)
return
})
}
err := b.Join()
AssertEq(nil, err)
// The symlink should have been created, once.
entries, err := ReadDirPicky(dir)
AssertEq(nil, err)
AssertEq(2, len(entries))
AssertEq("foo", entries[0].Name())
AssertEq("original_file", entries[1].Name())
// Remove the link.
err = os.Remove(filename)
AssertEq(nil, err)
}
// Clean up the original file at the end.
err = os.Remove(originalFile)
AssertEq(nil, err)
}

View File

@ -43,6 +43,7 @@ type FileSystem interface {
MkDir(context.Context, *fuseops.MkDirOp) error
MkNode(context.Context, *fuseops.MkNodeOp) error
CreateFile(context.Context, *fuseops.CreateFileOp) error
CreateLink(context.Context, *fuseops.CreateLinkOp) error
CreateSymlink(context.Context, *fuseops.CreateSymlinkOp) error
Rename(context.Context, *fuseops.RenameOp) error
RmDir(context.Context, *fuseops.RmDirOp) error
@ -159,6 +160,9 @@ func (s *fileSystemServer) handleOp(
case *fuseops.CreateFileOp:
err = s.fs.CreateFile(ctx, typed)
case *fuseops.CreateLinkOp:
err = s.fs.CreateLink(ctx, typed)
case *fuseops.CreateSymlinkOp:
err = s.fs.CreateSymlink(ctx, typed)

View File

@ -92,6 +92,13 @@ func (fs *NotImplementedFileSystem) CreateSymlink(
return
}
func (fs *NotImplementedFileSystem) CreateLink(
ctx context.Context,
op *fuseops.CreateLinkOp) (err error) {
err = fuse.ENOSYS
return
}
func (fs *NotImplementedFileSystem) Rename(
ctx context.Context,
op *fuseops.RenameOp) (err error) {

View File

@ -424,6 +424,46 @@ func (fs *memFS) CreateSymlink(
return
}
func (fs *memFS) CreateLink(
ctx context.Context,
op *fuseops.CreateLinkOp) (err error) {
fs.mu.Lock()
defer fs.mu.Unlock()
// Grab the parent, which we will update shortly.
parent := fs.getInodeOrDie(op.Parent)
// Ensure that the name doesn't already exist, so we don't wind up with a
// duplicate.
_, _, exists := parent.LookUpChild(op.Name)
if exists {
err = fuse.EEXIST
return
}
// Get the target inode to be linked
target := fs.getInodeOrDie(op.Target)
// Update the attributes
now := time.Now()
target.attrs.Nlink++
target.attrs.Ctime = now
// Add an entry in the parent.
parent.AddChild(op.Target, op.Name, fuseutil.DT_File)
// Return the response.
op.Entry.Child = op.Target
op.Entry.Attributes = target.attrs
// We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation).
op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
op.Entry.EntryExpiration = op.Entry.EntryExpiration
return
}
func (fs *memFS) Rename(
ctx context.Context,
op *fuseops.RenameOp) (err error) {

View File

@ -21,6 +21,7 @@ import (
"os"
"os/user"
"path"
"reflect"
"runtime"
"strconv"
"syscall"
@ -1088,26 +1089,6 @@ func (t *MemFSTest) ReadDirWhileModifying() {
ExpectTrue(namesSeen["qux"])
}
func (t *MemFSTest) HardLinks() {
var err error
// Create a file and a directory.
fileName := path.Join(t.Dir, "foo")
err = ioutil.WriteFile(fileName, []byte{}, 0400)
AssertEq(nil, err)
dirName := path.Join(t.Dir, "bar")
err = os.Mkdir(dirName, 0700)
AssertEq(nil, err)
// Attempt to link each. Neither should work, but for different reasons.
err = os.Link(fileName, path.Join(t.Dir, "baz"))
ExpectThat(err, Error(HasSubstr("not implemented")))
err = os.Link(dirName, path.Join(t.Dir, "baz"))
ExpectThat(err, Error(HasSubstr("not permitted")))
}
func (t *MemFSTest) CreateSymlink() {
var fi os.FileInfo
var err error
@ -1225,6 +1206,202 @@ func (t *MemFSTest) DeleteSymlink() {
ExpectThat(entries, ElementsAre())
}
func (t *MemFSTest) CreateHardlink() {
var fi os.FileInfo
var err error
// Create a file.
fileName := path.Join(t.Dir, "regular_file")
const contents = "Hello\x00world"
err = ioutil.WriteFile(fileName, []byte(contents), 0444)
AssertEq(nil, err)
// Clean up the file at the end.
defer func() {
err := os.Remove(fileName)
AssertEq(nil, err)
}()
// Create a link to the file.
linkName := path.Join(t.Dir, "foo")
err = os.Link(fileName, linkName)
AssertEq(nil, err)
// Clean up the file at the end.
defer func() {
err := os.Remove(linkName)
AssertEq(nil, err)
}()
// Stat the link.
fi, err = os.Lstat(linkName)
AssertEq(nil, err)
ExpectEq("foo", fi.Name())
ExpectEq(0444, fi.Mode())
// Read the parent directory.
entries, err := fusetesting.ReadDirPicky(t.Dir)
AssertEq(nil, err)
AssertEq(2, len(entries))
fi = entries[0]
ExpectEq("foo", fi.Name())
ExpectEq(0444, fi.Mode())
fi = entries[1]
ExpectEq("regular_file", fi.Name())
ExpectEq(0444, fi.Mode())
}
func (t *MemFSTest) CreateHardlink_AlreadyExists() {
var err error
// Create a file and a directory.
fileName := path.Join(t.Dir, "foo")
err = ioutil.WriteFile(fileName, []byte{}, 0400)
AssertEq(nil, err)
dirName := path.Join(t.Dir, "bar")
err = os.Mkdir(dirName, 0700)
AssertEq(nil, err)
// Create an existing symlink.
symlinkName := path.Join(t.Dir, "baz")
err = os.Symlink("blah", symlinkName)
AssertEq(nil, err)
// Create another link to the file.
hardlinkName := path.Join(t.Dir, "qux")
err = os.Link(fileName, hardlinkName)
AssertEq(nil, err)
// Symlinking on top of any of them should fail.
names := []string{
fileName,
dirName,
symlinkName,
hardlinkName,
}
for _, n := range names {
err = os.Link(fileName, n)
ExpectThat(err, Error(HasSubstr("exists")))
}
}
func (t *MemFSTest) DeleteHardlink() {
var fi os.FileInfo
var err error
// Create a file.
fileName := path.Join(t.Dir, "regular_file")
const contents = "Hello\x00world"
err = ioutil.WriteFile(fileName, []byte(contents), 0444)
AssertEq(nil, err)
// Step #1: We will create and remove a link and verify that
// after removal everything is as expected.
// Create a link to the file.
linkName := path.Join(t.Dir, "foo")
err = os.Link(fileName, linkName)
AssertEq(nil, err)
// Remove the link.
err = os.Remove(linkName)
AssertEq(nil, err)
// Stat the link.
fi, err = os.Lstat(linkName)
AssertEq(nil, fi)
ExpectThat(err, Error(HasSubstr("no such file")))
// Read the parent directory.
entries, err := fusetesting.ReadDirPicky(t.Dir)
AssertEq(nil, err)
AssertEq(1, len(entries))
fi = entries[0]
ExpectEq("regular_file", fi.Name())
ExpectEq(0444, fi.Mode())
// Step #2: We will create a link and remove the original file subsequently
// and verify that after removal everything is as expected.
// Create a link to the file.
linkName = path.Join(t.Dir, "bar")
err = os.Link(fileName, linkName)
AssertEq(nil, err)
// Remove the original file.
err = os.Remove(fileName)
AssertEq(nil, err)
// Stat the link.
fi, err = os.Lstat(linkName)
AssertEq(nil, err)
ExpectEq("bar", fi.Name())
ExpectEq(0444, fi.Mode())
// Stat the original file.
fi, err = os.Lstat(fileName)
AssertEq(nil, fi)
ExpectThat(err, Error(HasSubstr("no such file")))
// Read the parent directory.
entries, err = fusetesting.ReadDirPicky(t.Dir)
AssertEq(nil, err)
AssertEq(1, len(entries))
fi = entries[0]
ExpectEq("bar", fi.Name())
ExpectEq(0444, fi.Mode())
// Cleanup.
err = os.Remove(linkName)
AssertEq(nil, err)
}
func (t *MemFSTest) ReadHardlink() {
var err error
// Create a file.
fileName := path.Join(t.Dir, "regular_file")
const contents = "Hello\x00world"
err = ioutil.WriteFile(fileName, []byte(contents), 0444)
AssertEq(nil, err)
// Clean up the file at the end.
defer func() {
err := os.Remove(fileName)
AssertEq(nil, err)
}()
// Create a link to the file.
linkName := path.Join(t.Dir, "foo")
err = os.Link(fileName, linkName)
AssertEq(nil, err)
// Clean up the file at the end.
defer func() {
err := os.Remove(linkName)
AssertEq(nil, err)
}()
// Read files.
original, err := ioutil.ReadFile(fileName)
AssertEq(nil, err)
linked, err := ioutil.ReadFile(linkName)
AssertEq(nil, err)
// Check if the bytes are the same.
AssertEq(true, reflect.DeepEqual(original, linked))
}
func (t *MemFSTest) CreateInParallel_NoTruncate() {
fusetesting.RunCreateInParallelTest_NoTruncate(t.Ctx, t.Dir)
}
@ -1245,6 +1422,10 @@ func (t *MemFSTest) SymlinkInParallel() {
fusetesting.RunSymlinkInParallelTest(t.Ctx, t.Dir)
}
func (t *MemFSTest) HardlinkInParallel() {
fusetesting.RunHardlinkInParallelTest(t.Ctx, t.Dir)
}
func (t *MemFSTest) RenameWithinDir_File() {
var err error

View File

@ -445,3 +445,7 @@ func (t *PosixTest) MkdirInParallel() {
func (t *PosixTest) SymlinkInParallel() {
fusetesting.RunSymlinkInParallelTest(t.ctx, t.dir)
}
func (t *PosixTest) HardlinkInParallel() {
fusetesting.RunHardlinkInParallelTest(t.ctx, t.dir)
}