Skip to content
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

Interface->fs.FS translation logic #8

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
24 changes: 19 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,9 +26,25 @@ 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)
tester.DoBackendTests(t, b)
}

func TestFilesystem(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)
tester.DoFSWrapperTests(t, b)
}
4 changes: 2 additions & 2 deletions backends/memory/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ import (
)

func TestBackend(t *testing.T) {
b := New()
tester.DoBackendTests(t, b)
tester.DoBackendTests(t, New())
tester.DoFSWrapperTests(t, New())
Copy link
Member

Choose a reason for hiding this comment

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

I would fold this into DoBackendTests, so that that one always tests everything we would want to test about a backend. It could skip certain tests if the backend does not implement an optional backend.

}
9 changes: 9 additions & 0 deletions backends/s3/s3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ func TestBackend_marker(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "bar-1", string(data))
}

func TestFilesystem(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)

b := getBackend(ctx, t)
tester.DoFSWrapperTests(t, b)
assert.Equal(t, "", b.lastMarker)
}
73 changes: 73 additions & 0 deletions fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package simpleblob

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

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

// 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(st Interface) fs.FS {
Copy link
Member

Choose a reason for hiding this comment

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

Since all storage methods require a context and the FS interface itself does have have a way to provide one, I suggest passing it here and holding on to it in the wrapper. Using context.Background somewhere other than main is a bad idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, I'll push a fix. Thank you!

if fsys, ok := st.(fs.FS); ok {
return fsys
}
return &fsInterfaceWrapper{st}
}

// 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(context.Background(), name)
if err != nil {
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
}
return &fsBlobWrapper{&Blob{name, int64(len(b))}, stw, nil}, 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(context.Background(), 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(context.Background(), 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
}
57 changes: 57 additions & 0 deletions tester/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package tester

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

"github.com/PowerDNS/simpleblob"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// DoBackendTests tests a backend for conformance
Expand Down Expand Up @@ -72,3 +76,56 @@ func DoBackendTests(t *testing.T, b simpleblob.Interface) {
_, err = b.Load(ctx, "does-not-exist")
assert.ErrorIs(t, err, os.ErrNotExist)
}

// DoFSWrapperTests confronts Interface to its fs.FS implementations
func DoFSWrapperTests(t *testing.T, b simpleblob.Interface) {
// Wrap provided interface into a filesystem
// and use the backend to check operations on the filesystem.
// The backend is considered working from DoBackendTests.
fsys := simpleblob.AsFS(b)

// Opening random thing fails
f, err := fsys.Open("something")
assert.Error(t, err)
assert.Nil(t, f)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Test single item
fooData := make([]byte, 64)
_, err = rand.Read(fooData)
require.NoError(t, err)
err = b.Store(ctx, "foo", fooData)
require.NoError(t, err)

// Item can be loaded by name
f, err = fsys.Open("foo")
assert.NoError(t, err)
assert.NotNil(t, f)
defer func() {
assert.NoError(t, f.Close())
}()

// Item has right content
var p []byte
p, err = io.ReadAll(f)
require.NoError(t, err)
assert.Equal(t, p, fooData)

// Check file info
info, err := f.Stat()
assert.NoError(t, err)
assert.EqualValues(t, info.Mode(), 0777)
assert.Equal(t, info.Name(), "foo")
assert.EqualValues(t, info.Size(), 64)
assert.Equal(t, info.Sys(), fsys)

// fs.ReadFileFS is satisfied
p2, err := fs.ReadFile(fsys, "meh")
assert.Error(t, err)
assert.Empty(t, p2)
p2, err = fs.ReadFile(fsys, "foo")
assert.NoError(t, err)
assert.Equal(t, p, p2)
}