From 1b4a34cc7b6f1520582638ebe8afd24783f82125 Mon Sep 17 00:00:00 2001 From: Srdjan Rilak Date: Fri, 24 Nov 2017 12:44:41 +0100 Subject: [PATCH 1/2] Add CreateLink implementation --- conversions.go | 31 +++++++++++++++++++++++++ fuseops/ops.go | 20 ++++++++++++++++ fuseutil/file_system.go | 4 ++++ fuseutil/not_implemented_file_system.go | 7 ++++++ 4 files changed, 62 insertions(+) diff --git a/conversions.go b/conversions.go index 6b0f16e..66be34f 100644 --- a/conversions.go +++ b/conversions.go @@ -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 diff --git a/fuseops/ops.go b/fuseops/ops.go index a939799..56fe89c 100644 --- a/fuseops/ops.go +++ b/fuseops/ops.go @@ -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 //////////////////////////////////////////////////////////////////////// diff --git a/fuseutil/file_system.go b/fuseutil/file_system.go index fce16db..3a2a2cc 100644 --- a/fuseutil/file_system.go +++ b/fuseutil/file_system.go @@ -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) diff --git a/fuseutil/not_implemented_file_system.go b/fuseutil/not_implemented_file_system.go index b21e1e3..15cc1a0 100644 --- a/fuseutil/not_implemented_file_system.go +++ b/fuseutil/not_implemented_file_system.go @@ -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) { From 4ee295e33426bbfb39bf71a0bbae7303d0eaae6a Mon Sep 17 00:00:00 2001 From: Srdjan Rilak Date: Thu, 30 Nov 2017 14:54:33 +0100 Subject: [PATCH 2/2] Add tests for hard link --- fusetesting/parallel.go | 68 +++++++++++ samples/memfs/memfs.go | 40 +++++++ samples/memfs/memfs_test.go | 221 ++++++++++++++++++++++++++++++++---- samples/memfs/posix_test.go | 4 + 4 files changed, 313 insertions(+), 20 deletions(-) diff --git a/fusetesting/parallel.go b/fusetesting/parallel.go index 775c983..4bb19fb 100644 --- a/fusetesting/parallel.go +++ b/fusetesting/parallel.go @@ -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) +} diff --git a/samples/memfs/memfs.go b/samples/memfs/memfs.go index bf7c570..ae5c337 100644 --- a/samples/memfs/memfs.go +++ b/samples/memfs/memfs.go @@ -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) { diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index dcdad01..6e78a1b 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -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 diff --git a/samples/memfs/posix_test.go b/samples/memfs/posix_test.go index 50e947c..859f839 100644 --- a/samples/memfs/posix_test.go +++ b/samples/memfs/posix_test.go @@ -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) +}