Improved file persistence
This commit is contained in:
parent
e88f91e9d3
commit
edb369fe0a
161
Collection.go
161
Collection.go
@ -4,15 +4,18 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Collection[T any] interface {
|
type Collection[T any] interface {
|
||||||
All() <-chan T
|
All() <-chan *T
|
||||||
|
Clear()
|
||||||
Delete(key string)
|
Delete(key string)
|
||||||
Exists(key string) bool
|
Exists(key string) bool
|
||||||
Get(key string) (value T, ok bool)
|
Get(key string) (value *T, ok bool)
|
||||||
Set(key string, value T)
|
Set(key string, value *T)
|
||||||
}
|
}
|
||||||
|
|
||||||
// collection is a hash map of homogeneous data.
|
// collection is a hash map of homogeneous data.
|
||||||
@ -42,54 +45,16 @@ func NewCollection[T any](namespace string, name string) (*collection[T], error)
|
|||||||
directory: directory,
|
directory: directory,
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, err
|
return c, c.loadFromDisk()
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the value for the given key.
|
|
||||||
func (c *collection[T]) Get(key string) (T, bool) {
|
|
||||||
value, exists := c.data.Load(key)
|
|
||||||
return value.(T), exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets the value for the given key.
|
|
||||||
func (c *collection[T]) Set(key string, value T) {
|
|
||||||
c.data.Store(key, value)
|
|
||||||
|
|
||||||
fileName := path.Join(c.directory, key+".json")
|
|
||||||
file, err := os.Create(fileName)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(file)
|
|
||||||
encoder.Encode(value)
|
|
||||||
|
|
||||||
err = file.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a key from the collection.
|
|
||||||
func (c *collection[T]) Delete(key string) {
|
|
||||||
c.data.Delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns whether or not the key exists.
|
|
||||||
func (c *collection[T]) Exists(key string) bool {
|
|
||||||
_, exists := c.data.Load(key)
|
|
||||||
return exists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns a channel of all objects in the collection.
|
// All returns a channel of all objects in the collection.
|
||||||
func (c *collection[T]) All() <-chan T {
|
func (c *collection[T]) All() <-chan *T {
|
||||||
channel := make(chan T)
|
channel := make(chan *T)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
c.data.Range(func(key, value any) bool {
|
c.data.Range(func(key, value any) bool {
|
||||||
channel <- value.(T)
|
channel <- value.(*T)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -98,3 +63,109 @@ func (c *collection[T]) All() <-chan T {
|
|||||||
|
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get returns the value for the given key.
|
||||||
|
func (c *collection[T]) Get(key string) (*T, bool) {
|
||||||
|
value, exists := c.data.Load(key)
|
||||||
|
return value.(*T), exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the value for the given key.
|
||||||
|
func (c *collection[T]) Set(key string, value *T) {
|
||||||
|
c.data.Store(key, value)
|
||||||
|
c.writeFileToDisk(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a key from the collection.
|
||||||
|
func (c *collection[T]) Delete(key string) {
|
||||||
|
c.data.Delete(key)
|
||||||
|
os.Remove(c.keyFile(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists returns whether or not the key exists.
|
||||||
|
func (c *collection[T]) Exists(key string) bool {
|
||||||
|
_, exists := c.data.Load(key)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear deletes all objects from the collection.
|
||||||
|
func (c *collection[T]) Clear() {
|
||||||
|
c.data.Range(func(key, value any) bool {
|
||||||
|
c.Delete(key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyFile returns the file path for the given key.
|
||||||
|
func (c *collection[T]) keyFile(key string) string {
|
||||||
|
return filepath.Join(c.directory, key+".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFromDisk loads the collection data from the disk.
|
||||||
|
func (c *collection[T]) loadFromDisk() error {
|
||||||
|
file, err := os.Open(c.directory)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := file.Readdirnames(0)
|
||||||
|
|
||||||
|
for _, name := range files {
|
||||||
|
fileError := c.loadFileFromDisk(name)
|
||||||
|
|
||||||
|
if fileError != nil {
|
||||||
|
return fileError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFileFromDisk loads a single file from the disk.
|
||||||
|
func (c *collection[T]) loadFileFromDisk(name string) error {
|
||||||
|
file, err := os.Open(filepath.Join(c.directory, name))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value := new(T)
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
err = decoder.Decode(value)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
c.data.Store(key, value)
|
||||||
|
return file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFileToDisk writes the value for the key to disk as a JSON file.
|
||||||
|
func (c *collection[T]) writeFileToDisk(key string, value *T) {
|
||||||
|
fileName := c.keyFile(key)
|
||||||
|
file, err := os.Create(fileName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
err = encoder.Encode(value)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,45 +7,36 @@ import (
|
|||||||
"git.akyoto.dev/go/ocean"
|
"git.akyoto.dev/go/ocean"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCollectionGet(t *testing.T) {
|
type User struct {
|
||||||
users, err := ocean.NewCollection[string]("test", "User")
|
Name string `json:"name"`
|
||||||
assert.Nil(t, err)
|
}
|
||||||
|
|
||||||
users.Set("1", "abc")
|
func TestCollectionGet(t *testing.T) {
|
||||||
|
users, err := ocean.NewCollection[User]("test", "User")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer users.Clear()
|
||||||
|
|
||||||
|
users.Set("1", &User{Name: "User 1"})
|
||||||
user, exists := users.Get("1")
|
user, exists := users.Get("1")
|
||||||
|
|
||||||
assert.True(t, exists)
|
assert.True(t, exists)
|
||||||
assert.NotNil(t, user)
|
assert.NotNil(t, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectionAll(t *testing.T) {
|
|
||||||
users, err := ocean.NewCollection[string]("test", "User")
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
users.Set("1", "abc")
|
|
||||||
users.Set("2", "def")
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
for range users.All() {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, count, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInteraction(t *testing.T) {
|
func TestInteraction(t *testing.T) {
|
||||||
users, err := ocean.NewCollection[string]("test", "User")
|
users, err := ocean.NewCollection[User]("test", "User")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
defer users.Clear()
|
||||||
|
|
||||||
assert.True(t, !users.Exists("1"))
|
assert.True(t, !users.Exists("1"))
|
||||||
assert.True(t, !users.Exists("2"))
|
assert.True(t, !users.Exists("2"))
|
||||||
|
|
||||||
users.Set("1", "abc")
|
users.Set("1", &User{Name: "User 1"})
|
||||||
|
|
||||||
assert.True(t, users.Exists("1"))
|
assert.True(t, users.Exists("1"))
|
||||||
assert.True(t, !users.Exists("2"))
|
assert.True(t, !users.Exists("2"))
|
||||||
|
|
||||||
users.Set("2", "def")
|
users.Set("2", &User{Name: "User 1"})
|
||||||
|
|
||||||
assert.True(t, users.Exists("1"))
|
assert.True(t, users.Exists("1"))
|
||||||
assert.True(t, users.Exists("2"))
|
assert.True(t, users.Exists("2"))
|
||||||
@ -60,3 +51,19 @@ func TestInteraction(t *testing.T) {
|
|||||||
assert.True(t, !users.Exists("1"))
|
assert.True(t, !users.Exists("1"))
|
||||||
assert.True(t, !users.Exists("2"))
|
assert.True(t, !users.Exists("2"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCollectionAll(t *testing.T) {
|
||||||
|
users, err := ocean.NewCollection[User]("test", "User")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer users.Clear()
|
||||||
|
|
||||||
|
users.Set("1", &User{Name: "User 1"})
|
||||||
|
users.Set("2", &User{Name: "User 2"})
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
for range users.All() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, count, 2)
|
||||||
|
}
|
||||||
|
@ -4,4 +4,7 @@ In-memory key value store that saves your data to plain old JSON files.
|
|||||||
|
|
||||||
If you like, you can operate on your entire data with classic UNIX tools.
|
If you like, you can operate on your entire data with classic UNIX tools.
|
||||||
|
|
||||||
Writing to disk is async, so the performance of your code is only limited by your CPU and RAM speed.
|
```go
|
||||||
|
users := ocean.NewCollection("namespace", "User")
|
||||||
|
users.Set("1", &User{Name: "User 1"})
|
||||||
|
```
|
||||||
|
Loading…
Reference in New Issue
Block a user