Skip to content

Interface->fs.FS translation logic #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
14 changes: 9 additions & 5 deletions backends/fs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import (
"github.com/PowerDNS/simpleblob/tester"
)

func TestBackend(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "simpleblob-test-")
assert.NoError(t, err)
t.Cleanup(func() {
func cleanup(t *testing.T, tmpDir string) func() {
return func() {
// Don't want to use the recursive os.RemoveAll() for safety
if tmpDir == "" {
return
Expand All @@ -28,7 +26,13 @@ func TestBackend(t *testing.T) {
}
err = os.Remove(tmpDir)
assert.NoError(t, err)
})
}
}

func TestBackend(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "simpleblob-test-")
assert.NoError(t, err)
t.Cleanup(cleanup(t, tmpDir))

b, err := New(Options{RootPath: tmpDir})
assert.NoError(t, err)
Expand Down
3 changes: 1 addition & 2 deletions backends/memory/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ import (
)

func TestBackend(t *testing.T) {
b := New()
tester.DoBackendTests(t, b)
tester.DoBackendTests(t, New())
}
95 changes: 95 additions & 0 deletions fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package simpleblob

import (
"bytes"
"context"
"io/fs"
"time"
)

// fsInterfaceWrapper wraps an Interface and implements fs.FS.
type fsInterfaceWrapper struct {
Interface
ctx context.Context
}

// fsBlobWrapper represents data upstream and implements both fs.File
// and fs.FileInfo for convenience.
type fsBlobWrapper struct {
b *Blob
parent *fsInterfaceWrapper
r *bytes.Reader
}

// AsFS casts the provided interface to a fs.FS interface if supported,
// else it wraps it to replicate its functionalities.
func AsFS(ctx context.Context, st Interface) fs.FS {
if fsys, ok := st.(fs.FS); ok {
return fsys
}
return &fsInterfaceWrapper{st, ctx}
}

// Open retrieves a Blob, wrapped as a fs.File, from the underlying Interface.
func (stw *fsInterfaceWrapper) Open(name string) (fs.File, error) {
b, err := stw.Load(stw.ctx, name)
if err != nil {
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
}
return &fsBlobWrapper{&Blob{name, int64(len(b))}, stw, nil}, nil
}

// ReadDir satisfies fs.ReadDirFS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be best if we do not implement extra FS interface and instead stick to the basic fs.FS, but ReadDirFS could be a useful addition and corresponds to the List call.

I'm not sure if the single root directory restriction is problematic or not, I can imagine that we may want to store blocs with / in the future. Currently we do not clearly describe what kind of names are allowed and this could vary by backend, which is probably not a good thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove the name checking. I feel like fs.ReadFileFS should stay too because it matches what Load does. What do you think?

func (stw *fsInterfaceWrapper) ReadDir(name string) ([]fs.DirEntry, error) {
ls, err := stw.List(stw.ctx, "")
if err != nil {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
}
ret := make([]fs.DirEntry, len(ls))
for i, entry := range ls {
blob := entry
ret[i] = &fsBlobWrapper{&blob, stw, nil}
}
return ret, nil
}

// ReadFile implements fs.ReadFileFS on top of an Interface wrapped as a fs.FS.
func (stw *fsInterfaceWrapper) ReadFile(name string) ([]byte, error) {
return stw.Load(stw.ctx, name)
}

// fs.FileInfo implementation

func (*fsBlobWrapper) IsDir() bool { return false }
func (*fsBlobWrapper) ModTime() time.Time { return time.Time{} }
func (*fsBlobWrapper) Mode() fs.FileMode { return 0777 }
func (bw *fsBlobWrapper) Name() string { return bw.b.Name }
func (bw *fsBlobWrapper) Sys() interface{} { return bw.parent }
func (bw *fsBlobWrapper) Size() int64 { return bw.b.Size }

// fs.File implementation

func (bw *fsBlobWrapper) Stat() (fs.FileInfo, error) {
return bw, nil
}
func (bw *fsBlobWrapper) Read(p []byte) (int, error) {
if bw.r == nil {
b, err := bw.parent.Interface.Load(bw.parent.ctx, bw.b.Name)
if err != nil {
return 0, err
}
bw.r = bytes.NewReader(b)
}
return bw.r.Read(p)
}
func (bw *fsBlobWrapper) Close() error {
if bw.r != nil {
bw.r = nil
}
return nil
}
Comment on lines +75 to +90
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: use simpleblob.NewReader once #11 is merged


// fs.DirEntry implementation

func (bw *fsBlobWrapper) Type() fs.FileMode { return bw.Mode() }
func (bw *fsBlobWrapper) Info() (fs.FileInfo, error) { return bw, nil }
73 changes: 72 additions & 1 deletion tester/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tester

import (
"context"
"io"
"io/fs"
"os"
"testing"

Expand All @@ -14,10 +16,19 @@ func DoBackendTests(t *testing.T, b simpleblob.Interface) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Wrap provided interface into a filesystem
// and use the backend to check operations on the filesystem.
// Such operations will be performed along the ones on the backend.
fsys := simpleblob.AsFS(context.Background(), b)

// Starts empty
ls, err := b.List(ctx, "")
assert.NoError(t, err)
assert.Len(t, ls, 0)
// With FS
dirls, err := fs.ReadDir(fsys, ".")
assert.NoError(t, err)
assert.Len(t, dirls, 0)

// Add items
foo := []byte("foo") // will be modified later
Expand All @@ -33,9 +44,17 @@ func DoBackendTests(t *testing.T, b simpleblob.Interface) {
assert.NoError(t, err)

// List all
expectedNames := []string{"bar-1", "bar-2", "foo-1"} // sorted
ls, err = b.List(ctx, "")
assert.NoError(t, err)
assert.Equal(t, ls.Names(), []string{"bar-1", "bar-2", "foo-1"}) // sorted
assert.Equal(t, ls.Names(), expectedNames)
// With FS
dirls, err = fs.ReadDir(fsys, ".")
assert.NoError(t, err)
assert.Len(t, dirls, len(expectedNames))
for i, entry := range dirls {
assert.Equal(t, expectedNames[i], entry.Name())
}

// List with prefix
ls, err = b.List(ctx, "foo-")
Expand All @@ -50,27 +69,79 @@ func DoBackendTests(t *testing.T, b simpleblob.Interface) {
data, err := b.Load(ctx, "foo-1")
assert.NoError(t, err)
assert.Equal(t, data, []byte("foo"))
// With FS
f, err := fsys.Open("foo-1")
assert.NoError(t, err)
dataf, err := io.ReadAll(f)
assert.NoError(t, err)
assert.Equal(t, data, dataf)
assert.NoError(t, f.Close())
// With ReadFileFS
datar, err := fs.ReadFile(fsys, "foo-1")
assert.NoError(t, err)
assert.Equal(t, data, datar)

// Check overwritten data
data, err = b.Load(ctx, "bar-1")
assert.NoError(t, err)
assert.Equal(t, data, []byte("bar1"))
// With FS
f, err = fsys.Open("bar-1")
assert.NoError(t, err)
dataf, err = io.ReadAll(f)
assert.NoError(t, err)
assert.Equal(t, data, dataf)
assert.NoError(t, f.Close())
// With ReadFileFS
datar, err = fs.ReadFile(fsys, "bar-1")
assert.NoError(t, err)
assert.Equal(t, data, datar)

// Verify that Load makes a copy
data[0] = '!'
data, err = b.Load(ctx, "bar-1")
assert.NoError(t, err)
assert.Equal(t, data, []byte("bar1"))
// With FS
f, err = fsys.Open("bar-1")
assert.NoError(t, err)
dataf, err = io.ReadAll(f)
assert.NoError(t, err)
assert.Equal(t, data, dataf)
assert.NoError(t, f.Close())
// With ReadFileFS
datar, err = fs.ReadFile(fsys, "bar-1")
assert.NoError(t, err)
assert.Equal(t, data, datar)

// Change foo buffer to verify that Store made a copy
foo[0] = '!'
data, err = b.Load(ctx, "foo-1")
assert.NoError(t, err)
assert.Equal(t, data, []byte("foo"))
// With FS
f, err = fsys.Open("foo-1")
assert.NoError(t, err)
dataf, err = io.ReadAll(f)
assert.NoError(t, err)
assert.Equal(t, data, dataf)
assert.NoError(t, f.Close())
// With ReadFileFS
datar, err = fs.ReadFile(fsys, "foo-1")
assert.NoError(t, err)
assert.Equal(t, data, datar)

// Load non-existing
_, err = b.Load(ctx, "does-not-exist")
assert.ErrorIs(t, err, os.ErrNotExist)
// With FS
f, err = fsys.Open("something")
assert.ErrorIs(t, err, os.ErrNotExist)
assert.Nil(t, f)
// With ReadFileFS
datar, err = fs.ReadFile(fsys, "does-not-exist-either")
assert.Error(t, err)
assert.Empty(t, datar)

// Delete existing
err = b.Delete(ctx, "foo-1")
Expand Down