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 }