Repository: ceph/go-ceph
Pull request #225 (2020-04-17 17:44:51)

cephfs: add file open/close and read/write/seek and unlink
Add cephfs functions related to regular files including:
* open
* close
* read
* write
* seek

Additionally, we add unlink to the paths.go file so that the files tests can clean up after themselves.

## 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: @nixpanic I think my most recent patches should cover the cases that concerned you. PTAL, thanks.
phlogistonjohn: @ansiwen any additional feedback/discussion?
@nixpanic any thoughts on the current version?

Patch:
diff --git a/cephfs/errors.go b/cephfs/errors.go
index fa42c3e..fdbecb3 100644
--- a/cephfs/errors.go
+++ b/cephfs/errors.go
@@ -34,8 +34,18 @@ func getError(e C.int) error {
 	return CephFSError(e)
 }
 
+// Public go errors:
+
+const (
+	// ErrNotConnected may be returned when client is not connected
+	// to a cluster.
+	ErrNotConnected = CephFSError(-C.ENOTCONN)
+)
+
 // Private errors:
 
 const (
 	errNameTooLong = CephFSError(-C.ENAMETOOLONG)
+
+	errInvalid = CephFSError(-C.EINVAL)
 )
diff --git a/cephfs/file.go b/cephfs/file.go
new file mode 100644
index 0000000..07ba2e0
--- /dev/null
+++ b/cephfs/file.go
@@ -0,0 +1,175 @@
+package cephfs
+
+/*
+#cgo LDFLAGS: -lcephfs
+#cgo CPPFLAGS: -D_FILE_OFFSET_BITS=64
+#include <stdlib.h>
+#include <cephfs/libcephfs.h>
+*/
+import "C"
+
+import (
+	"io"
+	"unsafe"
+)
+
+const (
+	// SeekSet is used with Seek to set the absolute position in the file.
+	SeekSet = int(C.SEEK_SET)
+	// SeekCur is used with Seek to position the file relative to the current
+	// position.
+	SeekCur = int(C.SEEK_CUR)
+	// SeekEnd is used with Seek to position the file relative to the end.
+	SeekEnd = int(C.SEEK_END)
+)
+
+// File represents an open file descriptor in cephfs.
+type File struct {
+	mount *MountInfo
+	fd    C.int
+}
+
+// Open a file at the given path. The flags are the same os flags as
+// a local open call. Mode is the same mode bits as a local open call.
+//
+// Implements:
+//  int ceph_open(struct ceph_mount_info *cmount, const char *path, int flags, mode_t mode);
+func (mount *MountInfo) Open(path string, flags int, mode uint32) (*File, error) {
+	if mount.mount == nil {
+		return nil, ErrNotConnected
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	ret := C.ceph_open(mount.mount, cPath, C.int(flags), C.mode_t(mode))
+	if ret < 0 {
+		return nil, getError(ret)
+	}
+	return &File{mount: mount, fd: ret}, nil
+}
+
+func (f *File) validate() error {
+	if f.mount == nil {
+		return ErrNotConnected
+	}
+	return nil
+}
+
+// Close the file.
+//
+// Implements:
+//  int ceph_close(struct ceph_mount_info *cmount, int fd);
+func (f *File) Close() error {
+	if f.fd == -1 {
+		// already closed
+		return nil
+	}
+	if err := f.validate(); err != nil {
+		return err
+	}
+	if err := getError(C.ceph_close(f.mount.mount, f.fd)); err != nil {
+		return err
+	}
+	f.fd = -1
+	return nil
+}
+
+// read directly wraps the ceph_read call. Because read is such a common
+// operation we deviate from the ceph naming and expose Read and ReadAt
+// wrappers for external callers of the library.
+//
+// Implements:
+//  int ceph_read(struct ceph_mount_info *cmount, int fd, char *buf, int64_t size, int64_t offset);
+func (f *File) read(buf []byte, offset int64) (int, error) {
+	if err := f.validate(); err != nil {
+		return 0, err
+	}
+	bufptr := (*C.char)(unsafe.Pointer(&buf[0]))
+	ret := C.ceph_read(
+		f.mount.mount, f.fd, bufptr, C.int64_t(len(buf)), C.int64_t(offset))
+	switch {
+	case ret < 0:
+		return 0, getError(ret)
+	case ret == 0:
+		return 0, io.EOF
+	}
+	return int(ret), nil
+}
+
+// Read data from file. Up to len(buf) bytes will be read from the file.
+// The number of bytes read will be returned.
+// When nothing is left to read from the file, Read returns, 0, io.EOF.
+func (f *File) Read(buf []byte) (int, error) {
+	// to-consider: should we mimic Go's behavior of returning an
+	// io.ErrShortWrite error if write length < buf size?
+	return f.read(buf, -1)
+}
+
+// ReadAt will read data from the file starting at the given offset.
+// Up to len(buf) bytes will be read from the file.
+// The number of bytes read will be returned.
+// When nothing is left to read from the file, ReadAt returns, 0, io.EOF.
+func (f *File) ReadAt(buf []byte, offset int64) (int, error) {
+	if offset < 0 {
+		return 0, errInvalid
+	}
+	return f.read(buf, offset)
+}
+
+// write directly wraps the ceph_write call. Because write is such a common
+// operation we deviate from the ceph naming and expose Write and WriteAt
+// wrappers for external callers of the library.
+//
+// Implements:
+//  int ceph_write(struct ceph_mount_info *cmount, int fd, const char *buf,
+//                 int64_t size, int64_t offset);
+func (f *File) write(buf []byte, offset int64) (int, error) {
+	if err := f.validate(); err != nil {
+		return 0, err
+	}
+	bufptr := (*C.char)(unsafe.Pointer(&buf[0]))
+	ret := C.ceph_write(
+		f.mount.mount, f.fd, bufptr, C.int64_t(len(buf)), C.int64_t(offset))
+	if ret < 0 {
+		return 0, getError(ret)
+	}
+	return int(ret), nil
+}
+
+// Write data from buf to the file.
+// The number of bytes written is returned.
+func (f *File) Write(buf []byte) (int, error) {
+	return f.write(buf, -1)
+}
+
+// WriteAt writes data from buf to the file at the specified offset.
+// The number of bytes written is returned.
+func (f *File) WriteAt(buf []byte, offset int64) (int, error) {
+	if offset < 0 {
+		return 0, errInvalid
+	}
+	return f.write(buf, offset)
+}
+
+// Seek will reposition the file stream based on the given offset.
+//
+// Implements:
+//  int64_t ceph_lseek(struct ceph_mount_info *cmount, int fd, int64_t offset, int whence);
+func (f *File) Seek(offset int64, whence int) (int64, error) {
+	if err := f.validate(); err != nil {
+		return 0, err
+	}
+	// validate the seek whence value in case the caller skews
+	// from the seek values we technically support from C as documented.
+	// TODO: need to support seek-(hole|data) in mimic and later.
+	switch whence {
+	case SeekSet, SeekCur, SeekEnd:
+	default:
+		return 0, errInvalid
+	}
+
+	ret := C.ceph_lseek(f.mount.mount, f.fd, C.int64_t(offset), C.int(whence))
+	if ret < 0 {
+		return 0, getError(C.int(ret))
+	}
+	return int64(ret), nil
+}
diff --git a/cephfs/path.go b/cephfs/path.go
index 5bde60e..a47b86f 100644
--- a/cephfs/path.go
+++ b/cephfs/path.go
@@ -44,3 +44,15 @@ func (mount *MountInfo) RemoveDir(path string) error {
 	ret := C.ceph_rmdir(mount.mount, cPath)
 	return getError(ret)
 }
+
+// Unlink removes a file.
+//
+// Implements:
+//  int ceph_unlink(struct ceph_mount_info *cmount, const char *path);
+func (mount *MountInfo) Unlink(path string) error {
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+
+	ret := C.ceph_unlink(mount.mount, cPath)
+	return getError(ret)
+}


Test patch:
diff --git a/cephfs/file_test.go b/cephfs/file_test.go
new file mode 100644
index 0000000..2dc9e46
--- /dev/null
+++ b/cephfs/file_test.go
@@ -0,0 +1,339 @@
+package cephfs
+
+import (
+	"io"
+	"os"
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFileOpen(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestFileOpen.txt"
+
+	// idempotent open for read and write
+	t.Run("create", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f1)
+		err = f1.Close()
+		assert.NoError(t, err)
+		assert.NoError(t, mount.Unlink(fname))
+	})
+
+	t.Run("errorMissing", func(t *testing.T) {
+		// try to open a file we know should not exist
+		f2, err := mount.Open(".nope", os.O_RDONLY, 0666)
+		assert.Error(t, err)
+		assert.Nil(t, f2)
+	})
+
+	t.Run("existsInMount", func(t *testing.T) {
+		useMount(t)
+
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f1)
+		err = f1.Close()
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		s, err := os.Stat(path.Join(CephMountDir, fname))
+		assert.NoError(t, err)
+		assert.EqualValues(t, 0, s.Size())
+	})
+
+	t.Run("idempotentClose", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		assert.NotNil(t, f1)
+		assert.NoError(t, f1.Close())
+		assert.NoError(t, f1.Close()) // call close again. it should not fail
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+	})
+
+	t.Run("uninitializedFileClose", func(t *testing.T) {
+		f := &File{}
+		err := f.Close()
+		assert.Error(t, err)
+		assert.Equal(t, ErrNotConnected, err)
+	})
+
+	t.Run("invalidFdClose", func(t *testing.T) {
+		f := &File{mount, 1980}
+		err := f.Close()
+		assert.Error(t, err)
+	})
+
+	t.Run("openInvalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		_, err := m.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.Error(t, err)
+	})
+}
+
+func TestFileReadWrite(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestFileReadWrite.txt"
+
+	t.Run("writeAndRead", func(t *testing.T) {
+		// idempotent open for read and write
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		n, err := f1.Write([]byte("yello world!"))
+		assert.NoError(t, err)
+		assert.EqualValues(t, 12, n)
+		err = f1.Close()
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		buf := make([]byte, 1024)
+		f2, err := mount.Open(fname, os.O_RDONLY, 0)
+		n, err = f2.Read(buf)
+		assert.NoError(t, err)
+		assert.EqualValues(t, 12, n)
+		assert.Equal(t, "yello world!", string(buf[:n]))
+	})
+
+	t.Run("openForWriteOnly", func(t *testing.T) {
+		buf := make([]byte, 32)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		_, err = f1.Read(buf)
+		assert.Error(t, err)
+	})
+
+	t.Run("openForReadOnly", func(t *testing.T) {
+		// "touch" the file
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		assert.NoError(t, f1.Close())
+
+		f1, err = mount.Open(fname, os.O_RDONLY, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		_, err = f1.Write([]byte("yo"))
+		assert.Error(t, err)
+	})
+
+	t.Run("uninitializedFile", func(t *testing.T) {
+		f := &File{}
+		b := []byte("testme")
+		_, err := f.Write(b)
+		assert.Error(t, err)
+		assert.Equal(t, ErrNotConnected, err)
+		_, err = f.Read(b)
+		assert.Error(t, err)
+		assert.Equal(t, ErrNotConnected, err)
+	})
+}
+
+func TestFileReadWriteAt(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestFileReadWriteAt.txt"
+
+	t.Run("writeAtAndReadAt", func(t *testing.T) {
+		// idempotent open for read and write
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		n, err := f1.WriteAt([]byte("foo"), 0)
+		assert.NoError(t, err)
+		assert.EqualValues(t, 3, n)
+		n, err = f1.WriteAt([]byte("bar"), 6)
+		assert.NoError(t, err)
+		assert.EqualValues(t, 3, n)
+		// assert that negative offsets return an error
+		_, err = f1.WriteAt([]byte("baz"), -10)
+		assert.Error(t, err)
+		err = f1.Close()
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		buf := make([]byte, 4)
+		f2, err := mount.Open(fname, os.O_RDONLY, 0)
+		n, err = f2.ReadAt(buf, 0)
+		assert.NoError(t, err)
+		assert.EqualValues(t, 4, n)
+		assert.Equal(t, "foo", string(buf[:3]))
+		assert.EqualValues(t, 0, string(buf[3]))
+		n, err = f2.ReadAt(buf, 6)
+		assert.NoError(t, err)
+		assert.EqualValues(t, 3, n)
+		assert.Equal(t, "bar", string(buf[:3]))
+		// assert that negative offsets return an error
+		_, err = f2.ReadAt(buf, -10)
+		assert.Error(t, err)
+	})
+
+	t.Run("openForWriteOnly", func(t *testing.T) {
+		buf := make([]byte, 32)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		_, err = f1.ReadAt(buf, 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("openForReadOnly", func(t *testing.T) {
+		// "touch" the file
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		assert.NoError(t, f1.Close())
+
+		f1, err = mount.Open(fname, os.O_RDONLY, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		_, err = f1.WriteAt([]byte("yo"), 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("uninitializedFile", func(t *testing.T) {
+		f := &File{}
+		b := []byte("testme")
+		_, err := f.WriteAt(b, 0)
+		assert.Error(t, err)
+		assert.Equal(t, ErrNotConnected, err)
+		_, err = f.ReadAt(b, 0)
+		assert.Error(t, err)
+		assert.Equal(t, ErrNotConnected, err)
+	})
+}
+
+func TestFileInterfaces(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestFileInterfaces.txt"
+
+	t.Run("ioWriter", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		var w io.Writer = f1
+		_, err = w.Write([]byte("foo"))
+		assert.NoError(t, err)
+	})
+
+	t.Run("ioReader", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		_, err = f1.Write([]byte("foo"))
+		assert.NoError(t, err)
+		assert.NoError(t, f1.Close())
+
+		f1, err = mount.Open(fname, os.O_RDONLY, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		var r io.Reader = f1
+		buf := make([]byte, 32)
+		_, err = r.Read(buf)
+		assert.NoError(t, err)
+		n, err := r.Read(buf)
+		assert.Equal(t, 0, n)
+		assert.Equal(t, io.EOF, err)
+	})
+}
+
+func TestFileSeek(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestFileSeek.txt"
+
+	t.Run("validSeek", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		o, err := f1.Seek(8, SeekSet)
+		assert.NoError(t, err)
+		assert.EqualValues(t, 8, o)
+
+		n, err := f1.Write([]byte("flimflam"))
+		assert.NoError(t, err)
+		assert.EqualValues(t, 8, n)
+	})
+
+	t.Run("invalidWhence", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		o, err := f1.Seek(8, 1776)
+		assert.Error(t, err)
+		assert.EqualValues(t, 0, o)
+	})
+
+	t.Run("invalidSeek", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+		o, err := f1.Seek(-22, SeekSet)
+		assert.Error(t, err)
+		assert.EqualValues(t, 0, o)
+	})
+
+	t.Run("uninitializedFile", func(t *testing.T) {
+		f := &File{}
+		_, err := f.Seek(0, SeekSet)
+		assert.Error(t, err)
+		assert.Equal(t, ErrNotConnected, err)
+	})
+}
+
+func TestMixedReadReadAt(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestMixedReadReadAt.txt"
+
+	f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	assert.NoError(t, err)
+	_, err = f1.Write([]byte("abc def ghi wow!"))
+	assert.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	defer func() { assert.NoError(t, mount.Unlink(fname)) }()
+
+	buf := make([]byte, 4)
+	f2, err := mount.Open(fname, os.O_RDONLY, 0)
+	n, err := f2.ReadAt(buf, 0)
+	assert.NoError(t, err)
+	assert.Equal(t, 4, n)
+	assert.Equal(t, "abc ", string(buf))
+
+	n, err = f2.Read(buf)
+	assert.NoError(t, err)
+	assert.Equal(t, 4, n)
+	assert.Equal(t, "abc ", string(buf))
+
+	n, err = f2.Read(buf)
+	assert.NoError(t, err)
+	assert.Equal(t, 4, n)
+	assert.Equal(t, "def ", string(buf))
+
+	n, err = f2.ReadAt(buf, 0)
+	assert.NoError(t, err)
+	assert.Equal(t, 4, n)
+	assert.Equal(t, "abc ", string(buf))
+
+	n, err = f2.ReadAt(buf, 12)
+	assert.NoError(t, err)
+	assert.Equal(t, 4, n)
+	assert.Equal(t, "wow!", string(buf))
+
+	n, err = f2.Read(buf)
+	assert.NoError(t, err)
+	assert.Equal(t, 4, n)
+	assert.Equal(t, "ghi ", string(buf))
+
+	assert.NoError(t, f1.Close())
+}