From 854a834b1c8a023ef8b62e66564b247d419f8bdf Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Wed, 25 Mar 2020 17:31:13 -0400 Subject: [PATCH] cephfs: implement file IO functions for open/close/read/write/seek Implement core file I/O functions based on a file handle wrapper. Signed-off-by: John Mulligan --- cephfs/errors.go | 2 + cephfs/file.go | 133 +++++++++++++++++++++++++++ cephfs/file_test.go | 216 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 cephfs/file.go create mode 100644 cephfs/file_test.go diff --git a/cephfs/errors.go b/cephfs/errors.go index fa42c3e..ba3b095 100644 --- a/cephfs/errors.go +++ b/cephfs/errors.go @@ -38,4 +38,6 @@ func getError(e C.int) error { 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..3390889 --- /dev/null +++ b/cephfs/file.go @@ -0,0 +1,133 @@ +package cephfs + +/* +#cgo LDFLAGS: -lcephfs +#cgo CPPFLAGS: -D_FILE_OFFSET_BITS=64 +#include +#include +*/ +import "C" + +import ( + "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) { + 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 +} + +// Close the file. +// +// Implements: +// int ceph_close(struct ceph_mount_info *cmount, int fd); +func (f *File) Close() error { + return getError(C.ceph_close(f.mount.mount, f.fd)) +} + +// 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) { + 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)) + if ret < 0 { + return int(ret), getError(ret) + } + 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. +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. +func (f *File) ReadAt(buf []byte, offset int64) (int, error) { + 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) { + 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) { + 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) { + // 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/file_test.go b/cephfs/file_test.go new file mode 100644 index 0000000..3324322 --- /dev/null +++ b/cephfs/file_test.go @@ -0,0 +1,216 @@ +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) + // TODO: clean up file + }) + + 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) + + s, err := os.Stat(path.Join(CephMountDir, fname)) + assert.NoError(t, err) + assert.EqualValues(t, 0, s.Size()) + }) +} + +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) + + 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) + }) +} + +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) + err = f1.Close() + assert.NoError(t, err) + + 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])) + }) + + 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) + }) +} + +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()) }() + + 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) + 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()) }() + + var r io.Reader = f1 + buf := make([]byte, 32) + _, err = r.Read(buf) + assert.NoError(t, 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()) }() + + 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()) }() + + 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()) }() + + o, err := f1.Seek(-22, SeekSet) + assert.Error(t, err) + assert.EqualValues(t, 0, o) + }) +}