Repository: ceph/go-ceph
Pull request #236 (2020-04-29 16:00:17)

cephfs: adding Link, Symlink and Readlink functions
Link function implements ceph_link().
Symlink function implements ceph_symlink().
Readlink function implements ceph_readlink().
To fix https://github.com/ceph/go-ceph/issues/218

Signed-off-by: Mudit Agarwal <muagarwa@redhat.com>



## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code


phlogistonjohn: There's also a typo in the commit message:
`Symlink function implemens ceph_symlink().` "implemens" should be "implements"
Since you have to update things already it would nice to fix this while we can.

Patch:
diff --git a/cephfs/path.go b/cephfs/path.go
index a47b86f..865580e 100644
--- a/cephfs/path.go
+++ b/cephfs/path.go
@@ -56,3 +56,53 @@ func (mount *MountInfo) Unlink(path string) error {
 	ret := C.ceph_unlink(mount.mount, cPath)
 	return getError(ret)
 }
+
+// Link creates a new link to an existing file.
+//
+// Implements:
+//  int ceph_link (struct ceph_mount_info *cmount, const char *existing, const char *newname);
+func (mount *MountInfo) Link(oldname, newname string) error {
+	cOldname := C.CString(oldname)
+	defer C.free(unsafe.Pointer(cOldname))
+
+	cNewname := C.CString(newname)
+	defer C.free(unsafe.Pointer(cNewname))
+
+	ret := C.ceph_link(mount.mount, cOldname, cNewname)
+	return getError(ret)
+}
+
+// Symlink creates a symbolic link to an existing path.
+//
+// Implements:
+//  int ceph_symlink(struct ceph_mount_info *cmount, const char *existing, const char *newname);
+func (mount *MountInfo) Symlink(existing, newname string) error {
+	cExisting := C.CString(existing)
+	defer C.free(unsafe.Pointer(cExisting))
+
+	cNewname := C.CString(newname)
+	defer C.free(unsafe.Pointer(cNewname))
+
+	ret := C.ceph_symlink(mount.mount, cExisting, cNewname)
+	return getError(ret)
+}
+
+// Readlink returns the value of a symbolic link.
+//
+// Implements:
+//  int ceph_readlink(struct ceph_mount_info *cmount, const char *path, char *buf, int64_t size);
+func (mount *MountInfo) Readlink(path string) (string, error) {
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+
+	buf := make([]byte, 4096)
+	ret := C.ceph_readlink(mount.mount,
+		cPath,
+		(*C.char)(unsafe.Pointer(&buf[0])),
+		C.int64_t(len(buf)))
+	if ret < 0 {
+		return "", getError(ret)
+	}
+
+	return string(buf[:ret]), nil
+}


Test patch:
diff --git a/cephfs/path_test.go b/cephfs/path_test.go
index 2d593d4..bb8bd92 100644
--- a/cephfs/path_test.go
+++ b/cephfs/path_test.go
@@ -60,3 +60,300 @@ func TestRemoveDir(t *testing.T) {
 	assert.EqualError(t, err,
 		fmt.Sprintf("stat %s: no such file or directory", localPath))
 }
+
+func TestLink(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	t.Run("rootDirOperations", func(t *testing.T) {
+		// Root dir, both as source and destination.
+		err := mount.Link("/", "/")
+		// Error directory operations are not allowed.
+		assert.Error(t, err)
+
+		dir1 := "myDir1"
+		assert.NoError(t, mount.MakeDir(dir1, 0755))
+		defer func() {
+			assert.NoError(t, mount.RemoveDir(dir1))
+		}()
+
+		// Creating link for a directory.
+		err = mount.Link(dir1, "/")
+		// Error, directory operations not allowed.
+		assert.Error(t, err)
+	})
+
+	// Non-root directory operations.
+	fname := "testFile.txt"
+	dir2 := "myDir2"
+	assert.NoError(t, mount.MakeDir(dir2, 0755))
+	defer func() {
+		assert.NoError(t, mount.RemoveDir(dir2))
+	}()
+
+	t.Run("dirAsSource", func(t *testing.T) {
+		err := mount.Link(dir2, fname)
+		// Error, directory operations not allowed.
+		assert.Error(t, err)
+	})
+
+	t.Run("dirAsDestination", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE, 0666)
+		assert.NotNil(t, f1)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, f1.Close())
+			assert.NoError(t, mount.Unlink(fname))
+		}()
+		err = mount.Link(fname, dir2)
+		// Error, destination exists.
+		assert.Error(t, err)
+	})
+
+	// File operations.
+	t.Run("sourceDoesNotExist", func(t *testing.T) {
+		fname := "notExist.txt"
+		err := mount.Link(fname, "hardlnk")
+		// Error, file does not exist.
+		assert.Error(t, err)
+	})
+
+	t.Run("sourceExistsSuccess", func(t *testing.T) {
+		useMount(t)
+
+		fname1 := "TestFile1.txt"
+		f1, err := mount.Open(fname1, os.O_WRONLY|os.O_CREATE, 0666)
+		assert.NotNil(t, f1)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, f1.Close())
+			assert.NoError(t, mount.Unlink(fname1))
+		}()
+		err = mount.Link(fname1, "hardlnk")
+		defer func() { assert.NoError(t, mount.Unlink("hardlnk")) }()
+		// No error, normal link operation.
+		assert.NoError(t, err)
+		// Verify that link got created.
+		_, err = os.Stat(path.Join(CephMountDir, "hardlnk"))
+		assert.NoError(t, err)
+	})
+
+	t.Run("destExistsError", func(t *testing.T) {
+		// Create hard link when destination exists.
+		fname2 := "TestFile2.txt"
+		fname3 := "TestFile3.txt"
+		f2, err := mount.Open(fname2, os.O_WRONLY|os.O_CREATE, 0666)
+		assert.NotNil(t, f2)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, f2.Close())
+			assert.NoError(t, mount.Unlink(fname2))
+		}()
+		f3, err := mount.Open(fname3, os.O_WRONLY|os.O_CREATE, 0666)
+		assert.NotNil(t, f3)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, f3.Close())
+			assert.NoError(t, mount.Unlink(fname3))
+		}()
+		err = mount.Link(fname2, fname3)
+		// Error, destination already exists.
+		assert.Error(t, err)
+	})
+}
+
+func TestUnlink(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	t.Run("fileUnlink", func(t *testing.T) {
+		fname := "TestFile.txt"
+		err := mount.Unlink(fname)
+		// Error, file does not exist.
+		assert.Error(t, err)
+
+		f, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE, 0666)
+		assert.NotNil(t, f)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, f.Close())
+			assert.NoError(t, mount.Unlink(fname))
+		}()
+		assert.NoError(t, mount.Link(fname, "hardlnk"))
+
+		err = mount.Unlink("hardlnk")
+		// No Error, link will be removed.
+		assert.NoError(t, err)
+	})
+
+	t.Run("dirUnlink", func(t *testing.T) {
+		dirname := "/a"
+		err := mount.MakeDir(dirname, 0755)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, mount.RemoveDir(dirname))
+		}()
+
+		err = mount.Unlink(dirname)
+		// Error, not permitted on directory.
+		assert.Error(t, err)
+	})
+}
+
+func TestSymlink(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	// File operations.
+	t.Run("sourceDoesNotExistSuccess", func(t *testing.T) {
+		useMount(t)
+		fname1 := "TestFile1.txt"
+		err := mount.Symlink(fname1, "Symlnk1")
+		// No Error, symlink works even if source file doesn't exist.
+		assert.NoError(t, err)
+		_, err = os.Stat(path.Join(CephMountDir, "Symlnk1"))
+		// Error, source is not there.
+		assert.Error(t, err)
+
+		localPath := path.Join(CephMountDir, fname1)
+		_, err = os.Stat(localPath)
+		// Error, source file is still not there.
+		assert.Error(t, err)
+	})
+
+	t.Run("symlinkExistsError", func(t *testing.T) {
+		fname1 := "TestFile1.txt"
+		f1, err := mount.Open(fname1, os.O_RDWR|os.O_CREATE, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f1)
+		defer func() {
+			assert.NoError(t, f1.Close())
+			assert.NoError(t, mount.Unlink(fname1))
+		}()
+		err = mount.Symlink(fname1, "Symlnk1")
+		// Error, Symlink1 exists.
+		assert.Error(t, err)
+		defer func() {
+			assert.NoError(t, mount.Unlink("Symlnk1"))
+		}()
+	})
+
+	t.Run("sourceExistsSuccess", func(t *testing.T) {
+		useMount(t)
+		fname2 := "TestFile2.txt"
+		f2, err := mount.Open(fname2, os.O_RDWR|os.O_CREATE, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f2)
+		defer func() {
+			assert.NoError(t, f2.Close())
+			assert.NoError(t, mount.Unlink(fname2))
+		}()
+		err = mount.Symlink(fname2, "Symlnk2")
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, mount.Unlink("Symlnk2"))
+		}()
+		_, err = os.Stat(path.Join(CephMountDir, "Symlnk2"))
+		assert.NoError(t, err)
+	})
+
+	// Directory operations.
+	t.Run("rootDirOps", func(t *testing.T) {
+		err := mount.Symlink("/", "/")
+		assert.Error(t, err)
+
+		err = mount.Symlink("/", "someDir")
+		assert.NoError(t, err)
+
+		err = mount.Symlink("someFile", "/")
+		// Error, permission denied.
+		assert.Error(t, err)
+	})
+
+	t.Run("nonRootDir", func(t *testing.T) {
+		useMount(t)
+		// 1. Create a directory.
+		// 2. Create a symlink to that directory.
+		// 3. Create a file inside symlink.
+		// 4. Ensure that it is not a directory.
+		dirname := "mydir"
+		err := mount.MakeDir(dirname, 0755)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, mount.RemoveDir(dirname))
+		}()
+
+		err = mount.Symlink(dirname, "symlnk")
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, mount.Unlink("symlnk"))
+		}()
+
+		fname := "symlnk/file"
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f1)
+		defer func() {
+			assert.NoError(t, f1.Close())
+			assert.NoError(t, mount.Unlink(fname))
+		}()
+		var fileInfo os.FileInfo
+		fileInfo, err = os.Stat(path.Join(CephMountDir, "symlnk/file"))
+		assert.NoError(t, err)
+		assert.Equal(t, fileInfo.IsDir(), false)
+	})
+}
+
+func TestReadlink(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	t.Run("regularFile", func(t *testing.T) {
+		fname := "file1.txt"
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f1)
+		defer func() {
+			assert.NoError(t, f1.Close())
+			assert.NoError(t, mount.Unlink(fname))
+		}()
+
+		buf, err := mount.Readlink(fname)
+		// Error, given path is not symbolic link.
+		assert.Error(t, err)
+		assert.Equal(t, buf, "")
+	})
+
+	t.Run("symLink", func(t *testing.T) {
+		path1 := "path1"
+		path2 := "path2"
+		assert.NoError(t, mount.Symlink(path1, path2))
+		defer func() {
+			assert.NoError(t, mount.Unlink(path2))
+		}()
+		buf, err := mount.Readlink(path2)
+		assert.NoError(t, err)
+		assert.Equal(t, buf, path1)
+	})
+
+	t.Run("hardLink", func(t *testing.T) {
+		path3 := "path3"
+		path4 := "path4"
+		p, err := mount.Open(path3, os.O_RDWR|os.O_CREATE, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, p)
+		defer func() {
+			assert.NoError(t, p.Close())
+			assert.NoError(t, mount.Unlink(path3))
+		}()
+
+		assert.NoError(t, mount.Link(path3, path4))
+		defer func() {
+			assert.NoError(t, mount.Unlink(path4))
+		}()
+		buf, err := mount.Readlink(path4)
+		// Error, path4 is not symbolic link.
+		assert.Error(t, err)
+		assert.Equal(t, buf, "")
+	})
+}