filestore/filestore.go
2022-07-03 20:28:50 +02:00

159 lines
4 KiB
Go

// 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 (
"errors"
"fmt"
"io"
)
var (
errFileNotFound = errors.New("file not found")
errFilenameGeneration = errors.New("cannot generate filename")
)
// NameGenerator is the type of the functions that generate the filenames
// under which the files are stored on disk from thz name the user passed
// as argument.
type NameGenerator func(string) (string, error)
// NameIdentity returns the original filename unchanged.
func NameIdentity(originalName string) (string, error) {
return originalName, nil
}
// Option are options passed to the Fikeqtore constructors.
type Option func(*Filestore)
// WithNameGen allows to set a custom name generator on the filestore.
func WithNameGen(f NameGenerator) Option {
return func(fs *Filestore) {
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
// get files in a simple way.
type Filestore struct {
nameGenerator NameGenerator
buckets map[string]*Bucket
dataDir Backend
}
// NewFilestore creates a new filestore.
func NewFilestore(opts ...Option) (*Filestore, error) {
f := &Filestore{
dataDir: &MemoryBackend{},
nameGenerator: NameIdentity,
buckets: map[string]*Bucket{},
}
for i := range opts {
opts[i](f)
}
return f, nil
}
// Bucket gets a bucket from the filestore. If the asked bucket does not exist,
// it is created.
func (f *Filestore) Bucket(name string) *Bucket {
if b, ok := f.buckets[name]; ok {
return b
}
b := &Bucket{
dataDir: f.dataDir.Sub(name),
nameGenerator: f.nameGenerator,
}
f.buckets[name] = b
return b
}
// Bucket is a storage unit in the filestore.
type Bucket struct {
nameGenerator func(string) (string, error)
dataDir Backend
}
// Put stores a new file in the bucket.
func (b Bucket) Put(originalName string, r io.Reader) (string, error) {
var (
finalName string
err error
)
finalName, err = b.nameGenerator(originalName)
if err != nil {
return "", err
}
fd, err := b.dataDir.Create(finalName)
if err != nil {
return "", fmt.Errorf("cannot store %q: %w", originalName, err)
}
defer func() { _ = fd.Close() }() //nolint:errcheck // nothing to handle the error
if _, err := io.Copy(fd, r); err != nil {
return "", fmt.Errorf("cannot write the content of file %q to %q: %w",
originalName, finalName, err)
}
return finalName, nil
}
// Get retrieves a file from the bucket.
func (b Bucket) Get(name string) (io.ReadCloser, error) {
if !b.Exists(name) {
return nil, errFileNotFound
}
r, err := b.dataDir.Open(name)
if err != nil {
return nil, fmt.Errorf("cannot open file %q in bucket: %w", name, err)
}
return r, nil
}
// Exists checks the existence of a file in the bucket.
func (b Bucket) Exists(filename string) bool {
return b.dataDir.Exists(filename)
}
// Delete removes a file from the bucket. It is removed from storage and cannot
// be recovered.
// An error is returned if the file could not be removed.
func (b Bucket) Delete(filename string) error {
if err := b.dataDir.Delete(filename); err != nil {
return fmt.Errorf("cannot remove %q from bucket: %w", filename, err)
}
return nil
}
// SetNameGenerator sets the nameGenerator for this Bucket.
func (b *Bucket) SetNameGenerator(g func(string) (string, error)) {
b.nameGenerator = g
}