package store
// persistent key-value store
-// TODO: thread safety, persistence, efficient update and query, incremental backup
+// multiple key-value store can be managed by a single db connection
+// each store has a name, before usage the name of the store must be
+// ensured to exist
+//
+// TODO: this is a toy implementation
import (
- "fmt"
"os"
"path/filepath"
"io/ioutil"
- "sync"
)
type Conn struct {
- name string
path string
- mutex sync.RWMutex
- memstore map[string][]byte
}
-func Open(name string) (c *Conn, err error) {
+func Open(root string) (c *Conn, err error) {
c = new(Conn)
- c.memstore = make(map[string][]byte)
- c.name = name
- c.path, err = filepath.Abs(name)
+ c.path, err = filepath.Abs(root)
if err != nil {
return
}
if err != nil {
return
}
- err = filepath.Walk(c.path, func(path string, info *os.FileInfo, err error) error {
- if info.IsDirectory() {
- if info.Name == c.name {
- return nil
- }
- return filepath.SkipDir
- }
- if err != nil {
- return err
- }
- k := info.Name
- v, err := ioutil.ReadFile(filepath.Join(c.path, k))
- if err != nil {
- return err
- }
- c.memstore[k] = v
- return nil
- })
return
}
-func (c *Conn) Get(k string) (v []byte, err error) {
- c.mutex.RLock()
- defer c.mutex.RUnlock()
- v, ok := c.memstore[k]
- if !ok {
- err = fmt.Errorf("key not found")
- }
- return
+func (c *Conn) Get(name, k string) (v []byte, err error) {
+ return ioutil.ReadFile(filepath.Join(c.path, name, k))
+}
+
+func (c *Conn) Ensure(name string) (err error) {
+ return os.MkdirAll(filepath.Join(c.path, name), 0755)
}
-func (c *Conn) Set(k string, v []byte) (err error) {
- c.mutex.Lock()
- defer c.mutex.Unlock()
- f, err := os.Create(filepath.Join(c.path, k))
+func (c *Conn) Set(name, k string, v []byte) (err error) {
+ fn := filepath.Join(c.path, name, k)
+ // os.O_SYNC
+ f, err := os.Create(fn+".tmp")
if err != nil {
return
}
+ defer f.Close()
_, err = f.Write(v)
if err != nil {
return
}
- c.memstore[k] = v
+ err = os.Rename(fn+".tmp", fn)
return
}
--- /dev/null
+package store
+
+import (
+ "testing"
+)
+
+var testData = map[string]string {
+ "A" : "a",
+ "B" : "b",
+ "C" : "c",
+ "D" : "d",
+ "foo_bar-baz" : "a\nb\nc\nd\n",
+}
+
+func TestStore(t *testing.T) {
+ c, err := Open("teststore")
+ defer c.Close()
+ if err != nil {
+ t.Errorf("open failed: %s", err)
+ return
+ }
+ err = c.Ensure("abc")
+ if err != nil {
+ t.Errorf("ensure failed: %s", err)
+ return
+ }
+ for k, v := range testData {
+ err = c.Set("abc", k, []byte(v))
+ if err != nil {
+ t.Errorf("Set failed: %s", err)
+ }
+ }
+ for k, v := range testData {
+ d, err := c.Get("abc", k)
+ if err != nil {
+ t.Errorf("Get failed: %s", err)
+ continue
+ }
+ if string(d) != v {
+ t.Errorf("expected %s; got %s", v, string(d))
+ }
+ }
+}
+
+func TestPersist(t *testing.T) {
+ c, err := Open("teststore")
+ if err != nil {
+ t.Errorf("open failed: %s", err)
+ return
+ }
+ err = c.Ensure("abc")
+ if err != nil {
+ t.Errorf("ensure failed: %s", err)
+ return
+ }
+ for k, v := range testData {
+ err = c.Set("abc", k, []byte(v))
+ if err != nil {
+ t.Errorf("Set failed: %s", err)
+ }
+ }
+ c.Close()
+
+ c, err = Open("teststore")
+ if err != nil {
+ t.Errorf("open failed: %s", err)
+ return
+ }
+ for k, v := range testData {
+ d, err := c.Get("abc", k)
+ if err != nil {
+ t.Errorf("Get failed: %s", err)
+ continue
+ }
+ if string(d) != v {
+ t.Errorf("expected %s; got %s", v, string(d))
+ }
+ }
+}