From d85dca219581aaf0fcf4f20c53345c9c1c918a28 Mon Sep 17 00:00:00 2001 From: Dmitry Fedotov Date: Sun, 6 Jul 2025 22:59:08 +0300 Subject: [PATCH] init --- go.mod | 12 +++++ go.sum | 22 ++++++++ internal/filesystem/fs_storage.go | 71 ++++++++++++++++++++++++++ internal/filesystem/fs_storage_test.go | 45 ++++++++++++++++ internal/natsobj/nats_store.go | 69 +++++++++++++++++++++++++ storage.go | 38 ++++++++++++++ 6 files changed, 257 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/filesystem/fs_storage.go create mode 100644 internal/filesystem/fs_storage_test.go create mode 100644 internal/natsobj/nats_store.go create mode 100644 storage.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..829408e --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module code.uint32.ru/tiny/objstore + +go 1.24 + +require ( + github.com/nats-io/nats.go v1.41.2 + github.com/klauspost/compress v1.18.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sys v0.32.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5ebfc93 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU= +github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nats.go v1.41.2 h1:5UkfLAtu/036s99AhFRlyNDI1Ieylb36qbGjJzHixos= +github.com/nats-io/nats.go v1.41.2/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/internal/filesystem/fs_storage.go b/internal/filesystem/fs_storage.go new file mode 100644 index 0000000..864a08a --- /dev/null +++ b/internal/filesystem/fs_storage.go @@ -0,0 +1,71 @@ +package filesystem + +import ( + "fmt" + "os" + "path/filepath" +) + +func Open(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{prefix: abs}, nil +} + +type Storage struct { + prefix string +} + +func (s *Storage) Save(key string, data []byte) error { + path := s.toAbs(key) + + if err := os.WriteFile(path, data, 0664); err != nil { + return err + } + + return nil +} + +func (s *Storage) Load(key string) ([]byte, error) { + path := s.toAbs(key) + + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return b, nil +} + +func (s *Storage) Delete(key string) error { + path := s.toAbs(key) + + err := os.Remove(path) + if err != nil && os.IsNotExist(err) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Storage) Close() error { + return nil +} + +func (s *Storage) toAbs(path string) string { + return filepath.Join(s.prefix, path) +} diff --git a/internal/filesystem/fs_storage_test.go b/internal/filesystem/fs_storage_test.go new file mode 100644 index 0000000..9ea240d --- /dev/null +++ b/internal/filesystem/fs_storage_test.go @@ -0,0 +1,45 @@ +package filesystem + +import ( + "bytes" + "os" + "testing" +) + +func TestStorageMethods(t *testing.T) { + st, err := Open("./testdata") + if err != nil { + t.Fatal(err) + } + + name := "mytestfile" + data := []byte("contents of my test file") + + defer os.Remove(name) // just in case + + if err := st.Save(name, data); err != nil { + t.Fatal(err) + } + + if err := st.Save(name, data); err != nil { + t.Errorf("rewrite operatoin failed: %v", err) + } + + b, err := st.Load(name) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(data, b) { + t.Error("loaded file differs from original") + } + + if err := st.Delete(name); err != nil { + t.Errorf("delete failed: %v", err) + } + + if err := st.Delete(name); err != nil { + t.Errorf("delete of non-existent failed: %v", err) + } + +} diff --git a/internal/natsobj/nats_store.go b/internal/natsobj/nats_store.go new file mode 100644 index 0000000..3f12861 --- /dev/null +++ b/internal/natsobj/nats_store.go @@ -0,0 +1,69 @@ +package natsobj + +import ( + "github.com/nats-io/nats.go" +) + +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: "microkv 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 (n *Storage) Save(key string, data []byte) error { + if _, err := n.store.PutBytes(key, data); err != nil { + return err + } + return nil +} + +func (n *Storage) Load(key string) ([]byte, error) { + b, err := n.store.GetBytes(key) + if err != nil { + return nil, err + } + + return b, nil +} + +func (n *Storage) Delete(key string) error { + if err := n.store.Delete(key); err != nil { + return err + } + + return nil +} + +func (n *Storage) Close() error { + n.conn.Close() + + return nil +} diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..bb71077 --- /dev/null +++ b/storage.go @@ -0,0 +1,38 @@ +package microkv + +import ( + "code.uint32.ru/tiny/objstore/internal/filesystem" + "code.uint32.ru/tiny/objstore/internal/natsobj" +) + +var ( + _ Storage = (*natsobj.Storage)(nil) + _ Storage = (*filesystem.Storage)(nil) +) + +// Storage is a very basic object store. +type Storage interface { + // Save puts file with name 'key' into the store. If a file with such name + // already exists it gets overwritten. + Save(key string, data []byte) error + // Load returns contents of file named 'key'. + Load(key string) ([]byte, error) + // Delete removes file named 'key' from the store. + // If such file does not exist Delete returns nil. + Delete(key string) error + // Close must be called when you're done working with Storage. + Close() error +} + +// NewNats connects to NATS messaging system and tries to create +// a new object storage with name 'bucket'. The returned Storage +// uses the created bucket as underlying physical store. +func NewNats(bucket string, url string) (Storage, error) { + return natsobj.Open(bucket, url) +} + +// NewFS established a key/value within the directory 'path' +// and uses is as underlying physical store. +func NewFS(path string) (Storage, error) { + return filesystem.Open(path) +}