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)
This commit is contained in:
2025-07-27 19:02:05 +03:00
parent 854de3865b
commit 48878e8433
13 changed files with 265 additions and 153 deletions

View File

@@ -0,0 +1,8 @@
package errinternal
import "errors"
var (
ErrInvalidKey = errors.New("storage: invalid key")
ErrNotFound = errors.New("storage: not found")
)

View File

@@ -1,12 +1,26 @@
package filesystem
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"code.uint32.ru/tiny/storage/internal/errinternal"
)
func Open(path string) (*Storage, error) {
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
@@ -21,17 +35,24 @@ func Open(path string) (*Storage, error) {
return nil, fmt.Errorf("could not tarnslate %s to absolute path", path)
}
return &Storage{prefix: abs}, nil
return &Storage{Dir: abs}, nil
}
type Storage struct {
prefix string
Dir string
}
func (s *Storage) Save(key string, data []byte) error {
path := s.toAbs(key)
if err := validateKey(key); err != nil {
return err
}
if err := os.WriteFile(path, data, 0664); err != nil {
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
}
@@ -39,10 +60,16 @@ func (s *Storage) Save(key string, data []byte) error {
}
func (s *Storage) Load(key string) ([]byte, error) {
path := s.toAbs(key)
if err := validateKey(key); err != nil {
return nil, err
}
b, err := os.ReadFile(path)
if err != nil {
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
}
@@ -50,22 +77,47 @@ func (s *Storage) Load(key string) ([]byte, error) {
}
func (s *Storage) Delete(key string) error {
path := s.toAbs(key)
if err := validateKey(key); err != nil {
return err
}
err := os.Remove(path)
if err != nil && os.IsNotExist(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) Close() error {
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 (s *Storage) toAbs(path string) string {
return filepath.Join(s.prefix, path)
func getPrefixPath(key string) string {
r := []rune(key)
out := []rune{r[0], '/', r[1]}
return string(out)
}

View File

@@ -7,15 +7,16 @@ import (
)
func TestStorageMethods(t *testing.T) {
st, err := Open("./testdata")
st, err := New("./testdata")
if err != nil {
t.Fatal(err)
}
name := "mytestfile"
data := []byte("contents of my test file")
defer os.Remove(name) // just in case
defer os.RemoveAll("./testdata/m")
data := []byte("contents of my test file")
if err := st.Save(name, data); err != nil {
t.Fatal(err)
@@ -41,5 +42,4 @@ func TestStorageMethods(t *testing.T) {
if err := st.Delete(name); err != nil {
t.Errorf("delete of non-existent failed: %v", err)
}
}

View File

@@ -1,41 +1,22 @@
package natsobj
import (
"errors"
"code.uint32.ru/tiny/storage/internal/errinternal"
"github.com/nats-io/nats.go"
)
var (
ErrNotFound = errinternal.ErrNotFound
)
type Storage struct {
store nats.ObjectStore
conn *nats.Conn
}
func Open(bucket, url string) (*Storage, error) {
nc, err := nats.Connect(url)
if err != nil {
return nil, err
}
js, err := nc.JetStream()
if err != nil {
return nil, err
}
cfg := &nats.ObjectStoreConfig{
Bucket: bucket,
Description: "tiny storage bucket",
MaxBytes: -1,
Storage: nats.FileStorage,
Compression: true,
}
store, err := js.CreateObjectStore(cfg)
if err != nil {
return nil, err
}
st := &Storage{store: store, conn: nc}
return st, nil
func New(store nats.ObjectStore) *Storage {
return &Storage{store: store}
}
func (n *Storage) Save(key string, data []byte) error {
@@ -47,7 +28,9 @@ func (n *Storage) Save(key string, data []byte) error {
func (n *Storage) Load(key string) ([]byte, error) {
b, err := n.store.GetBytes(key)
if err != nil {
if err != nil && errors.Is(err, nats.ErrObjectNotFound) {
return nil, errors.Join(ErrNotFound, err)
} else if err != nil {
return nil, err
}
@@ -55,15 +38,12 @@ func (n *Storage) Load(key string) ([]byte, error) {
}
func (n *Storage) Delete(key string) error {
if err := n.store.Delete(key); err != nil {
err := n.store.Delete(key)
if err != nil && errors.Is(err, nats.ErrObjectNotFound) {
return nil
} else if err != nil {
return err
}
return nil
}
func (n *Storage) Close() error {
n.conn.Close()
return nil
}

View File

@@ -5,32 +5,26 @@ import (
"encoding/base64"
"errors"
"code.uint32.ru/tiny/storage/internal/errinternal"
"github.com/hashicorp/vault/api"
)
var (
ErrNotFound = errinternal.ErrNotFound
)
type Storage struct {
client *api.Client
path string
kv *api.KVv1
// TODO: kv2: *api.KVv2
}
func Open(token string, path string, addr string) (*Storage, error) {
conf := &api.Config{
Address: addr,
}
c, err := api.NewClient(conf)
if err != nil {
return nil, err
}
c.SetToken(token)
return &Storage{client: c, path: path}, nil
// New returns Storage writing to the specified vault path.
// Object will be base64 encoded and written to path/key.
func New(c *api.Client, path string) *Storage {
return &Storage{kv: c.KVv1(path)}
}
func (s *Storage) Save(key string, data []byte) error {
kv := s.client.KVv1(s.path)
str := base64.StdEncoding.EncodeToString(data)
m := map[string]any{
"data": map[string]string{
@@ -38,7 +32,7 @@ func (s *Storage) Save(key string, data []byte) error {
},
}
if err := kv.Put(context.Background(), "testkey", m); err != nil {
if err := s.kv.Put(context.Background(), "testkey", m); err != nil {
return err
}
@@ -46,10 +40,10 @@ func (s *Storage) Save(key string, data []byte) error {
}
func (s *Storage) Load(key string) ([]byte, error) {
kv := s.client.KVv1(s.path)
m, err := kv.Get(context.Background(), key)
if err != nil {
m, err := s.kv.Get(context.Background(), key)
if err != nil && errors.Is(err, api.ErrSecretNotFound) {
return nil, errors.Join(ErrNotFound, err)
} else if err != nil {
return nil, err
}
@@ -84,16 +78,9 @@ func (s *Storage) Load(key string) ([]byte, error) {
}
func (s *Storage) Delete(key string) error {
kv := s.client.KVv1(s.path)
if err := kv.Delete(context.Background(), key); err != nil {
if err := s.kv.Delete(context.Background(), key); err != nil {
return err
}
return nil
}
func (s *Storage) Close() error {
s.client.ClearToken()
return nil
}

View File

@@ -4,6 +4,8 @@ import (
"bytes"
"os"
"testing"
"code.uint32.ru/tiny/storage/storageutil"
)
func TestVaultStorage(t *testing.T) {
@@ -25,7 +27,9 @@ func TestVaultStorage(t *testing.T) {
t.Log(addr)
t.Log(path)
st, err := Open(token, path, addr)
client, err := storageutil.NewVaultApiClient(token, addr)
st := New(client, path)
if err != nil {
t.Fatal(err)
}