Files
storage/internal/filesystem/fs_storage.go
Dmitry Fedotov 3757a43318 feat: objects in vault are stored as a single secret
Co-authored-by: Dmitry Fedotov <dmitry@uint32.ru>
Co-committed-by: Dmitry Fedotov <dmitry@uint32.ru>
2025-08-10 13:02:38 +03:00

124 lines
2.5 KiB
Go

package filesystem
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"code.uint32.ru/tiny/storage/internal/errinternal"
)
const (
fileModeDir os.FileMode = 0755
fileModeFile os.FileMode = 0644
)
var (
ErrInvalidKey = errinternal.ErrInvalidKey
ErrNotFound = errinternal.ErrNotFound
)
func New(path string) (*Storage, error) {
info, err := os.Stat(path)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, fmt.Errorf("provided path %s is not a directory", path)
}
abs, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("could not tarnslate %s to absolute path", path)
}
return &Storage{Dir: abs}, nil
}
type Storage struct {
Dir string
}
func (s *Storage) Save(key string, data []byte) error {
if err := validateKey(key); err != nil {
return err
}
path := s.getKeyPath(key)
if err := os.MkdirAll(path, fileModeDir); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(path, key), data, fileModeFile); err != nil {
return err
}
return nil
}
func (s *Storage) Load(key string) ([]byte, error) {
if err := validateKey(key); err != nil {
return nil, err
}
path := s.getKeyPath(key)
b, err := os.ReadFile(filepath.Join(path, key))
if err != nil && errors.Is(err, os.ErrNotExist) {
return nil, errors.Join(errinternal.ErrNotFound, err)
} else if err != nil {
return nil, err
}
return b, nil
}
func (s *Storage) Delete(key string) error {
if err := validateKey(key); err != nil {
return err
}
path := s.getKeyPath(key)
err := os.Remove(filepath.Join(path, key))
if err != nil && errors.Is(err, os.ErrNotExist) {
return nil
} else if err != nil {
return err
}
// TODO: think of cleaning up path when no files left
// in /basedir/a/b/ after deleting key abc
return nil
}
func (s *Storage) getKeyPath(key string) string {
return filepath.Join(s.Dir, getPrefixPath(key))
}
func validateKey(key string) error {
if len([]rune(key)) < 3 {
return errors.Join(ErrInvalidKey, fmt.Errorf("key must be at least 3 characters long"))
}
// Of course windoze guys are missing the whole point, but
// let us use os-specific path separator and not ruin the whole fun
// for them :)
if strings.Contains(key, string(os.PathSeparator)) {
return errors.Join(ErrInvalidKey, fmt.Errorf("key must not contain path separator character: %s", string(os.PathSeparator)))
}
return nil
}
func getPrefixPath(key string) string {
r := []rune(key)
out := []rune{r[0], os.PathSeparator, r[1]}
return string(out)
}