// 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 }