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:
8
internal/errinternal/errors.go
Normal file
8
internal/errinternal/errors.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package errinternal
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidKey = errors.New("storage: invalid key")
|
||||
ErrNotFound = errors.New("storage: not found")
|
||||
)
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user