Repository: ceph/go-ceph
Pull request #403 (2020-11-11 21:38:38)

cephfs: add functions for path and file Truncate
The cephfs packages gains new functions:
* File method Truncate implementing ceph_ftruncate
* Truncate implementing ceph_truncate

As a bonus, while adding the above I noted the coverage on path.go was nearly 100% but just missing an error condition check for Statx, so I added it here.

Fixes: #264 


## 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: Rebased. @ansiwen @nixpanic @agarwal-mudit PTAL

Patch:
diff --git a/cephfs/file.go b/cephfs/file.go
index f8c1d95..83c49b8 100644
--- a/cephfs/file.go
+++ b/cephfs/file.go
@@ -391,3 +391,25 @@ func (f *File) Fsync(sync SyncChoice) error {
 func (f *File) Sync() error {
 	return f.Fsync(SyncAll)
 }
+
+// Truncate sets the size of the open file.
+// NOTE: In some versions of ceph a bug exists where calling ftruncate on a
+// file open for read-only is permitted. The go-ceph wrapper does no additional
+// checking and will inherit the issue on affected versions of ceph.  Please
+// refer to the following issue for details:
+// https://tracker.ceph.com/issues/48202
+//
+// Implements:
+//  int ceph_ftruncate(struct ceph_mount_info *cmount, int fd, int64_t size);
+func (f *File) Truncate(size int64) error {
+	if err := f.validate(); err != nil {
+		return err
+	}
+
+	ret := C.ceph_ftruncate(
+		f.mount.mount,
+		f.fd,
+		C.int64_t(size),
+	)
+	return getError(ret)
+}
diff --git a/cephfs/path.go b/cephfs/path.go
index c01e68d..cb5aa73 100644
--- a/cephfs/path.go
+++ b/cephfs/path.go
@@ -173,3 +173,22 @@ func (mount *MountInfo) Rename(from, to string) error {
 	ret := C.ceph_rename(mount.mount, cFrom, cTo)
 	return getError(ret)
 }
+
+// Truncate sets the size of the specified file.
+//
+// Implements:
+//  int ceph_truncate(struct ceph_mount_info *cmount, const char *path, int64_t size);
+func (mount *MountInfo) Truncate(path string, size int64) error {
+	if err := mount.validate(); err != nil {
+		return err
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+
+	ret := C.ceph_truncate(
+		mount.mount,
+		cPath,
+		C.int64_t(size),
+	)
+	return getError(ret)
+}


Test patch:
diff --git a/cephfs/file_test.go b/cephfs/file_test.go
index 517b989..56e9b0c 100644
--- a/cephfs/file_test.go
+++ b/cephfs/file_test.go
@@ -918,3 +918,54 @@ func TestFilePreadvPwritev(t *testing.T) {
 		assert.Error(t, err)
 	})
 }
+
+func TestFileTruncate(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	fname := "TestFileTruncate.txt"
+	defer mount.Unlink(fname)
+
+	t.Run("invalidSize", func(t *testing.T) {
+		// "touch" the file
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, f1.Close())
+		}()
+
+		err = f1.Truncate(-1)
+		assert.Error(t, err)
+
+		st, err := f1.Fstatx(StatxBasicStats, 0)
+		if assert.NoError(t, err) {
+			assert.EqualValues(t, 0, st.Size)
+		}
+	})
+
+	t.Run("closedFile", func(t *testing.T) {
+		t.Skip("test fails because of a bug(?) in ceph")
+		// "touch" the file
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		assert.NoError(t, f1.Close())
+
+		f2, err := mount.Open(fname, os.O_RDONLY, 0644)
+		assert.NoError(t, err)
+		assert.NoError(t, f2.Close())
+
+		err = f2.Truncate(1024)
+		assert.Error(t, err)
+
+		// I wanted to do the stat check here too but it is a pain to implement
+		// because we close the file.
+		// The original version of this test, using a read-only file, failed
+		// due to a bug in libcephfs (see Truncate doc comment).
+	})
+
+	t.Run("invalidFile", func(t *testing.T) {
+		f := &File{}
+		err := f.Truncate(0)
+		assert.Error(t, err)
+	})
+}
diff --git a/cephfs/path_test.go b/cephfs/path_test.go
index 127c820..c1dfefe 100644
--- a/cephfs/path_test.go
+++ b/cephfs/path_test.go
@@ -359,23 +359,31 @@ func TestReadlink(t *testing.T) {
 }
 
 func TestStatx(t *testing.T) {
-	mount := fsConnect(t)
-	defer fsDisconnect(t, mount)
+	t.Run("statPath", func(t *testing.T) {
+		mount := fsConnect(t)
+		defer fsDisconnect(t, mount)
 
-	dirname := "statme"
-	assert.NoError(t, mount.MakeDir(dirname, 0755))
+		dirname := "statme"
+		assert.NoError(t, mount.MakeDir(dirname, 0755))
 
-	st, err := mount.Statx(dirname, StatxBasicStats, 0)
-	assert.NoError(t, err)
-	assert.NotNil(t, st)
-	assert.Equal(t, uint16(0755), st.Mode&0777)
+		st, err := mount.Statx(dirname, StatxBasicStats, 0)
+		assert.NoError(t, err)
+		assert.NotNil(t, st)
+		assert.Equal(t, uint16(0755), st.Mode&0777)
 
-	assert.NoError(t, mount.RemoveDir(dirname))
+		assert.NoError(t, mount.RemoveDir(dirname))
 
-	st, err = mount.Statx(dirname, StatxBasicStats, 0)
-	assert.Error(t, err)
-	assert.Nil(t, st)
-	assert.Equal(t, errNoEntry, err)
+		st, err = mount.Statx(dirname, StatxBasicStats, 0)
+		assert.Error(t, err)
+		assert.Nil(t, st)
+		assert.Equal(t, errNoEntry, err)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		_, err := m.Statx("junk", StatxBasicStats, 0)
+		assert.Error(t, err)
+	})
 }
 
 func TestRename(t *testing.T) {
@@ -399,3 +407,52 @@ func TestRename(t *testing.T) {
 		assert.NoError(t, mount.RemoveDir(n2))
 	})
 }
+
+func TestTruncate(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	fname := "TestTruncate.txt"
+	defer mount.Unlink(fname)
+
+	// "touch" the file
+	f, err := mount.Open(fname, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+	assert.NoError(t, err)
+	assert.NoError(t, f.Close())
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		err := m.Truncate(fname, 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("invalidSize", func(t *testing.T) {
+		err := mount.Truncate(fname, -1)
+		assert.Error(t, err)
+	})
+
+	t.Run("invalidPath", func(t *testing.T) {
+		err := mount.Truncate(".Non~Existant~", 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("valid", func(t *testing.T) {
+		err := mount.Truncate(fname, 1024)
+		assert.NoError(t, err)
+
+		st, err := mount.Statx(fname, StatxBasicStats, 0)
+		if assert.NoError(t, err) {
+			assert.NotNil(t, st)
+			assert.EqualValues(t, 1024, st.Size)
+		}
+
+		err = mount.Truncate(fname, 0)
+		assert.NoError(t, err)
+
+		st, err = mount.Statx(fname, StatxBasicStats, 0)
+		if assert.NoError(t, err) {
+			assert.NotNil(t, st)
+			assert.EqualValues(t, 0, st.Size)
+		}
+	})
+}