filestore/filestore.go
2022-05-25 18:12:03 +02:00

156 lines
3.7 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
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
}
// FilestoreOption are options passed to the Fikeqtore constructors.
type FilestoreOption func(*Filestore)
// WithNameGen allows to set a custom name generator on the filestore.
func WithNameGen(f NameGenerator) FilestoreOption {
return func(fs *Filestore) {
fs.nameGenerator = f
}
}
// 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 string
}
// NewFilestore creates a new filestore.
func NewFilestore(path string, opts ...FilestoreOption) (*Filestore, error) {
f := &Filestore{
dataDir: path,
nameGenerator: NameIdentity,
buckets: map[string]*Bucket{},
}
for i := range opts {
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
}
// 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: filepath.Join(f.dataDir, 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 string
}
// 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
}
fullpath := filepath.Clean(filepath.Join(b.dataDir, finalName))
err = os.MkdirAll(filepath.Dir(fullpath), 0o700)
if err != nil {
return "", fmt.Errorf("cannot create filestore subdirectories: %w", 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
if _, err := io.Copy(fd, r); err != nil {
return "", fmt.Errorf("cannot write the content of file %s: %w", fullpath, 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 := os.Open(filepath.Clean(filepath.Join(b.dataDir, name)))
if err != nil {
return nil, fmt.Errorf("cannot open file %s: %w", name, err)
}
return r, nil
}
// Exists checks the existence of a file in the bucket.
func (b Bucket) Exists(filename string) bool {
if _, err := os.Stat(filepath.Join(b.dataDir, filename)); !os.IsNotExist(err) {
return true
}
return false
}
// 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 := os.Remove(filepath.Join(b.dataDir, filename)); err != nil {
return fmt.Errorf("cannot remove file: %w", err)
}
return nil
}
// SetNameGenerator sets the nameGenerator for this Bucket.
func (b *Bucket) SetNameGenerator(g func(string) (string, error)) {
b.nameGenerator = g
}