Improved file persistence

This commit is contained in:
Eduard Urbach 2023-07-06 14:10:12 +02:00
parent e88f91e9d3
commit edb369fe0a
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
3 changed files with 149 additions and 68 deletions

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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"})
```