Draft: Resolve "Move to use Io/FS to have a better FS abstraction" #2

Open
bcarlin wants to merge 2 commits from 1-move-to-use-io-fs-to-have-a-better-fs-abstraction into main
4 changed files with 542 additions and 187 deletions

140
backend.go Normal file
View file

@ -0,0 +1,140 @@
package filestore
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
// Backend defines the set of methods that a storage backend must implement.
// It is quite naïve and regroups what is used by the Filestore in order to
// handle its operations.
type Backend interface {
// Sub should return a new backend rooted in a subdirectory of the
// original backend.
Sub(path string) Backend
// Create should open a file for writing and return a type that
// implements WriteCloser . The caller MUST call close when the file is
// not needed anymore (typically when all the data has been written).
//
// It should not return an error if the file already exists, but
// instead truncate or replace the existing file.
Create(name string) (io.WriteCloser, error)
// Open should open a file for reading and return a type that
// implements ReadCloser . The caller MUST call close when the file is
// not needed anymore (typically when all the data has been read).
Open(name string) (io.ReadCloser, error)
// Exists should return true if the file exists and false otherwise.
Exists(name string) bool
// Delete should delete the actual file. It should not return an error
// if the file does not exist.
Delete(name string) error
}
type OSBackend struct {
path string
}
func NewOSBackend(path string) *OSBackend {
return &OSBackend{
path: path,
}
}
func (ob OSBackend) Sub(path string) Backend {
return &OSBackend{filepath.Join(ob.path, path)}
}
func (ob OSBackend) Create(name string) (io.WriteCloser, error) {
if err := os.MkdirAll(ob.path, 0o700); err != nil {
return nil, fmt.Errorf("cannot create the directory to write the file: %w", err)
}
file, err := os.OpenFile(filepath.Join(ob.path, name),
os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
if err != nil {
return nil, fmt.Errorf("cannot create file: %w", err)
}
return file, nil
}
func (ob OSBackend) Open(name string) (io.ReadCloser, error) {
file, err := os.Open(filepath.Join(ob.path, name))
if err != nil {
return nil, fmt.Errorf("cannot open file %q: %w", name, err)
}
return file, nil
}
func (ob OSBackend) Exists(name string) bool {
if _, err := os.Stat(filepath.Join(ob.path, name)); !os.IsNotExist(err) {
return true
}
return false
}
func (ob OSBackend) Delete(name string) error {
if !ob.Exists(name) {
return nil
}
if err := os.Remove(filepath.Join(ob.path, name)); err != nil {
return fmt.Errorf("cannot remove file %q: %w", name, err)
}
return nil
}
type MemoryFile struct {
*bytes.Buffer
}
func (MemoryFile) Close() error {
return nil
}
type MemoryBackend struct {
files map[string][]byte
}
func (mb *MemoryBackend) Sub(name string) Backend {
return new(MemoryBackend)
}
func (mb *MemoryBackend) Create(name string) (io.WriteCloser, error) {
if mb.files == nil {
mb.files = map[string][]byte{}
}
mb.files[name] = []byte{}
return MemoryFile{bytes.NewBuffer(mb.files[name])}, nil
}
func (mb *MemoryBackend) Open(name string) (io.ReadCloser, error) {
if b, ok := mb.files[name]; ok {
return MemoryFile{bytes.NewBuffer(b)}, nil
}
return nil, errors.New("file not found")
}
func (mb *MemoryBackend) Delete(name string) error {
delete(mb.files, name)
return nil
}
func (mb *MemoryBackend) Exists(name string) bool {
_, ok := mb.files[name]
return ok
}

242
backend_test.go Normal file
View file

@ -0,0 +1,242 @@
package filestore
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestMemoryBackend(t *testing.T) {
t.Parallel()
t.Run("it is a valid backend", func(t *testing.T) {
m := new(MemoryBackend)
ValidateBackend(t, m)
})
t.Run("read then write a file", func(t *testing.T) {
})
}
func TestOSBackend(t *testing.T) {
t.Parallel()
t.Run("it is a valid backend", func(t *testing.T) {
tmpDir := t.TempDir()
m := NewOSBackend(tmpDir)
ValidateBackend(t, m)
})
t.Run("it returns an error if the directory cannot be created", func(t *testing.T) {
assert := require.New(t)
tmpDir := t.TempDir()
fullpath := filepath.Join(tmpDir, "foo")
m := NewOSBackend(fullpath)
err := os.WriteFile(fullpath, []byte(filecontent), 0o600)
assert.NoError(err)
wc, err := m.Create("foo")
assert.Error(err)
assert.Nil(wc)
})
}
//revive:disable:cognitive-complexity
func ValidateBackend(t *testing.T, b Backend) {
t.Helper()
filecontent := []byte("file content")
setupBackend(t, b)
t.Run("validate Sub", func(t *testing.T) {
t.Run("it must not be nil", func(t *testing.T) {
s := b.Sub("foo")
require.NotNil(t, s, "Sub must not return a nil value")
})
})
t.Run("validate Create", func(t *testing.T) {
t.Run("it must create a file if it does not exist", func(t *testing.T) {
assert := require.New(t)
precondFileNotExists(t, b, "create-1")
wc, err := b.Create("create-1")
assert.NoError(err, "Create must not return an error in case of success")
assert.NotNil(wc, "Create must return a non nil value in case of success")
_, err = wc.Write(filecontent)
assert.NoError(err)
wc.Close()
assert.True(b.Exists("create-1"), "The file must exist after "+
"Create has been successfully called")
rc, err := b.Open("create-1")
assert.NoError(err)
newcontent, err := io.ReadAll(rc)
assert.NoError(err)
assert.Equal(filecontent, newcontent, "The content of the created "+
"file must be correct")
})
t.Run("it should replace a file if it exists", func(t *testing.T) {
assert := require.New(t)
precondFileExists(t, b, "create-2")
wc, err := b.Create("create-2")
assert.NoError(err, "Create must not return an error if the file "+
"already exists")
_, err = wc.Write(filecontent)
assert.NoError(err)
wc.Close()
rc, err := b.Open("create-2")
assert.NoError(err)
newcontent, err := io.ReadAll(rc)
assert.NoError(err)
assert.NotEqual([]byte("hello world"), newcontent, "Create should "+
"overwrite the file when it already existed")
assert.Equal(filecontent, newcontent, "Create should "+
"overwrite the file when it already existed")
})
t.Run("when the file cannot be created", func(t *testing.T) {
t.Skip("do not know how to implement that!")
// maybe we should require that a directory already exists in the backend
assert := require.New(t)
wc, err := b.Create("create-3")
assert.NoError(err)
assert.Nil(wc, "the file access must be nil if Create returns an error")
})
})
t.Run("validate Open", func(t *testing.T) {
t.Run("it should return an open file if it exists", func(t *testing.T) {
assert := require.New(t)
precondFileExists(t, b, "open-1")
rc, err := b.Open("open-1")
assert.NoError(err, "Open must not return an error when successful")
assert.NotNil(rc, "Open must not return a nil value when successful")
rc.Close()
})
t.Run("when the file does not exist", func(t *testing.T) {
assert := require.New(t)
precondFileNotExists(t, b, "open-2")
rc, err := b.Open("open-2")
assert.Error(err, "Open must return an error when the file does not exist")
assert.Nil(rc, "Open must return a nil value when the file does not exist")
})
t.Run("when the file cannot be opened", func(t *testing.T) {
t.Skip("do not know how to implement that test!")
precondFileExists(t, b, "open-3")
})
})
t.Run("validate Delete", func(t *testing.T) {
t.Run("it should remove a file if it exists", func(t *testing.T) {
assert := require.New(t)
precondFileExists(t, b, "delete-1")
err := b.Delete("delete-1")
assert.NoError(err, "Delete must not return an error in case of success")
assert.False(b.Exists("delete-1"), "File must not exist anymore "+
"after a successful delete")
})
t.Run("when the file does not exist", func(t *testing.T) {
assert := require.New(t)
precondFileNotExists(t, b, "delete-2")
err := b.Delete("delete-2")
assert.NoError(err, "Delete must not return an error if the file "+
"does not exist")
})
t.Run("when the file cannot be deleted", func(t *testing.T) {
t.Skip("don't know ho to test that!")
assert := require.New(t)
precondFileExists(t, b, "delete-3")
err := b.Delete("delete-3")
assert.Error(err, "Delete must return an error when the file "+
"cannot be deleted")
})
})
t.Run("validate Exists", func(t *testing.T) {
t.Run("it should return true if the file exists", func(t *testing.T) {
wc, err := b.Create("exists1")
require.NoError(t, err)
require.NotNil(t, wc)
_, err = wc.Write(filecontent)
wc.Close()
require.NoError(t, err)
require.True(t, b.Exists("exists1"))
})
t.Run("it should return false if the file does not exist", func(t *testing.T) {
require.False(t, b.Exists("exists2"))
})
})
}
func setupBackend(t *testing.T, b Backend) {
t.Helper()
assert := require.New(t)
wc, err := b.Create("create-2")
assert.NoError(err)
defer wc.Close()
_, err = wc.Write([]byte("hello world"))
assert.NoError(err)
wc, err = b.Create("open-1")
assert.NoError(err)
defer wc.Close()
_, err = wc.Write([]byte(filecontent))
assert.NoError(err)
wc, err = b.Create("delete-1")
assert.NoError(err)
defer wc.Close()
_, err = wc.Write([]byte(filecontent))
assert.NoError(err)
}
func precondFileExists(t *testing.T, b Backend, name string) {
t.Helper()
require.True(t, b.Exists(name), "PRECOND: file %q must exist for this test",
name)
}
func precondFileNotExists(t *testing.T, b Backend, name string) {
t.Helper()
require.False(t, b.Exists(name), "PRECOND: file %q must not exist for this test",
name)
}

View file

@ -1,11 +1,21 @@
package main // Package filestore offers an abstraction to store and retrieve files in
// an easy manner.
//
// The main component is the Filestore type, which has primitives to store
// and retrieve files in the selected backend.
//
// The files are stored in buckets, so that it is possible to apply custon
// storing rules (eg. pictures in a bucket and thumbnails in another one.
//
// The files actually stored in the backends are named according to the
// chosen NameGenerator, which allow one to use subdirectories in the
// bucket for organization or performance purposes.
package filestore
import ( import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath"
) )
var ( var (
@ -23,28 +33,35 @@ func NameIdentity(originalName string) (string, error) {
return originalName, nil return originalName, nil
} }
// FilestoreOption are options passed to the Fikeqtore constructors. // Option are options passed to the Fikeqtore constructors.
type FilestoreOption func(*Filestore) type Option func(*Filestore)
// WithNameGen allows to set a custom name generator on the filestore. // WithNameGen allows to set a custom name generator on the filestore.
func WithNameGen(f NameGenerator) FilestoreOption { func WithNameGen(f NameGenerator) Option {
return func(fs *Filestore) { return func(fs *Filestore) {
fs.nameGenerator = f fs.nameGenerator = f
} }
} }
// WithBackend specify the backend the fiilestore must use.
func WithBackend(b Backend) Option {
return func(fs *Filestore) {
fs.dataDir = b
}
}
// Filestore is an abstraction of the filesystem that allow one to store and // Filestore is an abstraction of the filesystem that allow one to store and
// get files in a simple way. // get files in a simple way.
type Filestore struct { type Filestore struct {
nameGenerator NameGenerator nameGenerator NameGenerator
buckets map[string]*Bucket buckets map[string]*Bucket
dataDir string dataDir Backend
} }
// NewFilestore creates a new filestore. // NewFilestore creates a new filestore.
func NewFilestore(path string, opts ...FilestoreOption) (*Filestore, error) { func NewFilestore(opts ...Option) (*Filestore, error) {
f := &Filestore{ f := &Filestore{
dataDir: path, dataDir: &MemoryBackend{},
nameGenerator: NameIdentity, nameGenerator: NameIdentity,
buckets: map[string]*Bucket{}, buckets: map[string]*Bucket{},
} }
@ -53,10 +70,6 @@ func NewFilestore(path string, opts ...FilestoreOption) (*Filestore, error) {
opts[i](f) opts[i](f)
} }
if err := os.MkdirAll(f.dataDir, 0o700); err != nil {
return nil, fmt.Errorf("cannot create the filestore directory: %w", err)
}
return f, nil return f, nil
} }
@ -68,7 +81,7 @@ func (f *Filestore) Bucket(name string) *Bucket {
} }
b := &Bucket{ b := &Bucket{
dataDir: filepath.Join(f.dataDir, name), dataDir: f.dataDir.Sub(name),
nameGenerator: f.nameGenerator, nameGenerator: f.nameGenerator,
} }
@ -80,7 +93,7 @@ func (f *Filestore) Bucket(name string) *Bucket {
// Bucket is a storage unit in the filestore. // Bucket is a storage unit in the filestore.
type Bucket struct { type Bucket struct {
nameGenerator func(string) (string, error) nameGenerator func(string) (string, error)
dataDir string dataDir Backend
} }
// Put stores a new file in the bucket. // Put stores a new file in the bucket.
@ -95,22 +108,16 @@ func (b Bucket) Put(originalName string, r io.Reader) (string, error) {
return "", err return "", err
} }
fullpath := filepath.Clean(filepath.Join(b.dataDir, finalName)) fd, err := b.dataDir.Create(finalName)
err = os.MkdirAll(filepath.Dir(fullpath), 0o700)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot create filestore subdirectories: %w", err) return "", fmt.Errorf("cannot store %q: %w", originalName, err)
}
fd, err := os.Create(fullpath)
if err != nil {
return "", fmt.Errorf("cannot create file %s: %w", fullpath, err)
} }
defer func() { _ = fd.Close() }() //nolint:errcheck // nothing to handle the error defer func() { _ = fd.Close() }() //nolint:errcheck // nothing to handle the error
if _, err := io.Copy(fd, r); err != nil { if _, err := io.Copy(fd, r); err != nil {
return "", fmt.Errorf("cannot write the content of file %s: %w", fullpath, err) return "", fmt.Errorf("cannot write the content of file %q to %q: %w",
originalName, finalName, err)
} }
return finalName, nil return finalName, nil
@ -122,9 +129,9 @@ func (b Bucket) Get(name string) (io.ReadCloser, error) {
return nil, errFileNotFound return nil, errFileNotFound
} }
r, err := os.Open(filepath.Clean(filepath.Join(b.dataDir, name))) r, err := b.dataDir.Open(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot open file %s: %w", name, err) return nil, fmt.Errorf("cannot open file %q in bucket: %w", name, err)
} }
return r, nil return r, nil
@ -132,19 +139,15 @@ func (b Bucket) Get(name string) (io.ReadCloser, error) {
// Exists checks the existence of a file in the bucket. // Exists checks the existence of a file in the bucket.
func (b Bucket) Exists(filename string) bool { func (b Bucket) Exists(filename string) bool {
if _, err := os.Stat(filepath.Join(b.dataDir, filename)); !os.IsNotExist(err) { return b.dataDir.Exists(filename)
return true
}
return false
} }
// Delete removes a file from the bucket. It is removed from storage and cannot // Delete removes a file from the bucket. It is removed from storage and cannot
// be recovered. // be recovered.
// An error is returned if the file could not be removed. // An error is returned if the file could not be removed.
func (b Bucket) Delete(filename string) error { func (b Bucket) Delete(filename string) error {
if err := os.Remove(filepath.Join(b.dataDir, filename)); err != nil { if err := b.dataDir.Delete(filename); err != nil {
return fmt.Errorf("cannot remove file: %w", err) return fmt.Errorf("cannot remove %q from bucket: %w", filename, err)
} }
return nil return nil

View file

@ -1,11 +1,9 @@
package main package filestore
import ( import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@ -26,15 +24,6 @@ func nameGenAddress(f NameGenerator) string {
return fmt.Sprintf("%p", f) return fmt.Sprintf("%p", f)
} }
func initStore(t *testing.T, path string) *Filestore {
t.Helper()
fs, err := NewFilestore(path)
require.NoError(t, err, "cannot initialize filestore")
return fs
}
func TestFilestore(t *testing.T) { func TestFilestore(t *testing.T) {
t.Parallel() t.Parallel()
@ -45,52 +34,18 @@ func TestFilestore(t *testing.T) {
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs, err := NewFilestore(tmpDir)
assert.NoError(err) assert.NoError(err)
assert.NotNil(fs) assert.NotNil(fs)
}) })
t.Run("It should create its directories if they do not exist", func(t *testing.T) {
t.Parallel()
assert := require.New(t)
tmpDir := t.TempDir()
fs, err := NewFilestore(filepath.Join(tmpDir, "foo", "bar", "baz"))
assert.NoError(err)
assert.NotNil(fs)
})
t.Run("Initialize a store in a directory that can't be created", func(t *testing.T) {
t.Parallel()
assert := require.New(t)
tmpDir := t.TempDir()
tmpDir = filepath.Join(tmpDir, "unwritable")
err := os.Mkdir(tmpDir, 0o000)
assert.NoError(err)
tmpDir = filepath.Join(tmpDir, "subdir")
os.WriteFile(tmpDir, []byte("foo"), 0o644)
fs, err := NewFilestore(tmpDir)
assert.Error(err)
assert.Nil(fs)
})
t.Run("Selecting a new bucket", func(t *testing.T) { t.Run("Selecting a new bucket", func(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs, err := NewFilestore(tmpDir)
assert.NoError(err) assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
@ -103,8 +58,7 @@ func TestFilestore(t *testing.T) {
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs, err := NewFilestore(tmpDir)
assert.NoError(err) assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
@ -121,13 +75,10 @@ func TestFilestore(t *testing.T) {
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs, err := NewFilestore(tmpDir)
assert.NoError(err) assert.NoError(err)
b := fs.Bucket(bucket) assert.Equal(nameGenAddress(NameIdentity), nameGenAddress(fs.nameGenerator))
assert.NotNil(b)
assert.Equal(nameGenAddress(NameIdentity), nameGenAddress(b.nameGenerator))
}) })
t.Run("A custom name generator can be used", func(t *testing.T) { t.Run("A custom name generator can be used", func(t *testing.T) {
@ -139,13 +90,32 @@ func TestFilestore(t *testing.T) {
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore(WithNameGen(nameGen))
fs, err := NewFilestore(tmpDir, WithNameGen(nameGen))
assert.NoError(err) assert.NoError(err)
b := fs.Bucket(bucket) assert.Equal(nameGenAddress(nameGen), nameGenAddress(fs.nameGenerator))
assert.NotNil(b) })
assert.Equal(nameGenAddress(nameGen), nameGenAddress(b.nameGenerator))
t.Run("The Default backend is memory", func(t *testing.T) {
t.Parallel()
assert := require.New(t)
fs, err := NewFilestore()
assert.NoError(err)
assert.IsType(fs.dataDir, &MemoryBackend{})
})
t.Run("A custom backend can be used", func(t *testing.T) {
t.Parallel()
assert := require.New(t)
fs, err := NewFilestore(WithBackend(NewOSBackend("foo")))
assert.NoError(err)
assert.IsType(fs.dataDir, &OSBackend{})
}) })
} }
@ -163,8 +133,7 @@ func TestBucket(t *testing.T) {
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs, err := NewFilestore(tmpDir)
assert.NoError(err) assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
@ -174,6 +143,27 @@ func TestBucket(t *testing.T) {
assert.NotEqual(nameGenAddress(fs.nameGenerator), nameGenAddress(b.nameGenerator)) assert.NotEqual(nameGenAddress(fs.nameGenerator), nameGenAddress(b.nameGenerator))
assert.Equal(nameGenAddress(nameGen), nameGenAddress(b.nameGenerator)) assert.Equal(nameGenAddress(nameGen), nameGenAddress(b.nameGenerator))
}) })
t.Run("the backend is the same as the file store", func(t *testing.T) {
t.Parallel()
assert := require.New(t)
fs, err := NewFilestore()
assert.NoError(err)
b := fs.Bucket(bucket)
assert.NotNil(b)
assert.IsType(b.dataDir, fs.dataDir)
fs, err = NewFilestore(WithBackend(NewOSBackend("foo")))
assert.NoError(err)
b = fs.Bucket(bucket)
assert.NotNil(b)
assert.IsType(b.dataDir, fs.dataDir)
})
} }
func TestBucketPut(t *testing.T) { func TestBucketPut(t *testing.T) {
@ -186,10 +176,11 @@ func TestBucketPut(t *testing.T) {
t.Run("It should store a file", func(t *testing.T) { t.Run("It should store a file", func(t *testing.T) {
t.Parallel() t.Parallel()
tmpDir := t.TempDir()
fs := initStore(t, tmpDir)
assert := require.New(t) assert := require.New(t)
fs, err := NewFilestore()
assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
fr := strings.NewReader(filecontent) fr := strings.NewReader(filecontent)
@ -197,9 +188,6 @@ func TestBucketPut(t *testing.T) {
name, err := b.Put(filename, fr) name, err := b.Put(filename, fr)
assert.NoError(err, "cannot store file") assert.NoError(err, "cannot store file")
assert.Equal("foo.bar", name) assert.Equal("foo.bar", name)
assert.FileExists(filepath.Join(tmpDir, bucket, filename),
"the file does not exist on disk")
defer b.Delete(filename)
assert.True(b.Exists(name), assert.True(b.Exists(name),
"the file does not exist in the store") "the file does not exist in the store")
@ -207,14 +195,11 @@ func TestBucketPut(t *testing.T) {
t.Run("It returns an error if the name cannot be generated", func(t *testing.T) { t.Run("It returns an error if the name cannot be generated", func(t *testing.T) {
t.Parallel() t.Parallel()
errNameGen := func(_ string) (string, error) { errNameGen := func(_ string) (string, error) {
return "", errFilenameGeneration return "", errFilenameGeneration
} }
tmpDir := t.TempDir()
assert := require.New(t) assert := require.New(t)
fs, err := NewFilestore(tmpDir, WithNameGen(errNameGen)) fs, err := NewFilestore(WithNameGen(errNameGen))
assert.NoError(err) assert.NoError(err)
fr := strings.NewReader(filecontent) fr := strings.NewReader(filecontent)
@ -224,48 +209,15 @@ func TestBucketPut(t *testing.T) {
assert.Equal("", name) assert.Equal("", name)
}) })
t.Run("It returns an error if the bucket dir cannot be created", func(t *testing.T) { t.Run("It returns an error if the file cannot be stored", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
assert := require.New(t)
fs, err := NewFilestore(tmpDir)
assert.NoError(err)
err = os.WriteFile(filepath.Join(tmpDir, bucket), []byte("foo"), 0o644)
assert.NoError(err)
err = os.Chmod(tmpDir, 0o400)
assert.NoError(err)
defer func() {
err = os.Chmod(tmpDir, 0o700)
assert.NoError(err)
}()
fr := strings.NewReader(filecontent)
name, err := fs.Bucket(bucket).Put(filename, fr)
assert.Error(err)
assert.Equal("", name)
})
t.Run("It returns an error if the file cannot be opened", func(t *testing.T) {
t.Parallel() t.Parallel()
t.Skip()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs, err := NewFilestore(tmpDir)
assert.NoError(err) assert.NoError(err)
bucketDir := filepath.Join(tmpDir, bucket)
err = os.Mkdir(bucketDir, 0o700)
assert.NoError(err)
err = os.Mkdir(filepath.Join(bucketDir, filename), 0o700)
assert.NoError(err)
fr := strings.NewReader(filecontent) fr := strings.NewReader(filecontent)
name, err := fs.Bucket(bucket).Put(filename, fr) name, err := fs.Bucket(bucket).Put(filename, fr)
@ -275,10 +227,9 @@ func TestBucketPut(t *testing.T) {
t.Run("It returns an error if the file cannot be written", func(t *testing.T) { t.Run("It returns an error if the file cannot be written", func(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir()
fs, err := NewFilestore(tmpDir) fs, err := NewFilestore()
assert.NoError(err) assert.NoError(err)
@ -300,24 +251,22 @@ func TestBucketGet(t *testing.T) {
t.Run("It should retrieve a file", func(t *testing.T) { t.Run("It should retrieve a file", func(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir()
fs := initStore(t, tmpDir) fs, err := NewFilestore()
assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
fullpath := filepath.Join(tmpDir, bucket, filename) r := strings.NewReader(filecontent)
err := os.Mkdir(filepath.Dir(fullpath), 0o700) _, err = b.Put(filename, r)
assert.NoError(err) assert.NoError(err)
err = os.WriteFile(fullpath, []byte(filecontent), 0o600)
assert.NoError(err)
defer b.Delete(filename)
r, err := b.Get(filename) rc, err := b.Get(filename)
assert.NoError(err) assert.NoError(err)
defer func() { _ = r.Close() }() defer func() { _ = rc.Close() }()
content, err := io.ReadAll(r) content, err := io.ReadAll(rc)
assert.NoError(err) assert.NoError(err)
assert.Equal(filecontent, string(content)) assert.Equal(filecontent, string(content))
}) })
@ -326,8 +275,9 @@ func TestBucketGet(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs := initStore(t, tmpDir) assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
r, err := b.Get(filename) r, err := b.Get(filename)
@ -337,21 +287,14 @@ func TestBucketGet(t *testing.T) {
t.Run("It returns an error if the file cannot be read", func(t *testing.T) { t.Run("It returns an error if the file cannot be read", func(t *testing.T) {
t.Parallel() t.Parallel()
t.Skip()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs := initStore(t, tmpDir) assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
fullpath := filepath.Join(tmpDir, bucket, filename)
err := os.Mkdir(filepath.Dir(fullpath), 0o700)
assert.NoError(err)
//revive:disable-next-line:add-constant // Only used here for tests
err = os.WriteFile(fullpath, []byte(filecontent), 0o100)
assert.NoError(err)
defer b.Delete(filename)
r, err := b.Get(filename) r, err := b.Get(filename)
assert.Error(err) assert.Error(err)
assert.Nil(r) assert.Nil(r)
@ -370,27 +313,24 @@ func TestBucketExists(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir() fs, err := NewFilestore()
fs := initStore(t, tmpDir) assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
wc, err := b.dataDir.Create(filename)
fullpath := filepath.Join(tmpDir, bucket, filename)
err := os.Mkdir(filepath.Dir(fullpath), 0o700)
assert.NoError(err) assert.NoError(err)
wc.Close()
err = os.WriteFile(fullpath, []byte(filecontent), 0o600)
assert.NoError(err)
defer b.Delete(filename)
assert.True(b.Exists(filename)) assert.True(b.Exists(filename))
}) })
t.Run("It returns false if the file does not exist", func(t *testing.T) { t.Run("It returns false if the file does not exist", func(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
tmpDir := t.TempDir()
fs := initStore(t, tmpDir) fs, err := NewFilestore()
assert.NoError(err)
b := fs.Bucket(bucket) b := fs.Bucket(bucket)
assert.False(b.Exists(filename)) assert.False(b.Exists(filename))
@ -405,13 +345,13 @@ func TestBucketDelete(t *testing.T) {
filename = "foo.bar" filename = "foo.bar"
) )
t.Run("Removing a file (happy path)", func(t *testing.T) { t.Run("Removing a file", func(t *testing.T) {
t.Parallel() t.Parallel()
tmpDir := t.TempDir()
fs := initStore(t, tmpDir)
assert := require.New(t) assert := require.New(t)
fs, err := NewFilestore()
assert.NoError(err)
fr := strings.NewReader(filecontent) fr := strings.NewReader(filecontent)
name, err := fs.Bucket(bucket).Put(filename, fr) name, err := fs.Bucket(bucket).Put(filename, fr)
@ -421,7 +361,37 @@ func TestBucketDelete(t *testing.T) {
err = fs.Bucket(bucket).Delete(name) err = fs.Bucket(bucket).Delete(name)
assert.NoError(err) assert.NoError(err)
assert.NoFileExists(filepath.Join(tmpDir, bucket, filename)) assert.False(fs.Bucket(bucket).Exists(name))
assert.False(fs.Bucket("foo").Exists(name)) })
t.Run("Removing a file that do not exists", func(t *testing.T) {
t.Parallel()
assert := require.New(t)
fs, err := NewFilestore()
assert.NoError(err)
err = fs.Bucket(bucket).Delete(filename)
assert.NoError(err)
})
t.Run("Removing a file with errors", func(t *testing.T) {
t.Skip()
t.Parallel()
assert := require.New(t)
fs, err := NewFilestore()
assert.NoError(err)
err = fs.Bucket(bucket).Delete(filename)
assert.Error(err)
}) })
} }
func TestIdentityNameGenerator(t *testing.T) {
t.Parallel()
name, err := NameIdentity("foo.txt")
require.NoError(t, err)
require.Equal(t, "foo.txt", name)
}