Files
storage/internal/filesystem/fs_storage.go
Dmitry Fedotov 48878e8433 feat: working version
1. implemented filesystem storage, NATS object storage
and saving to Vault.
2. Test coverage is fine for filesystem and Vault
(and NATS object does not really require extensive tests)
2025-07-27 19:02:05 +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], '/', r[1]}
return string(out)
}