diff --git a/kvdb/kvdb.go b/kvdb/kvdb.go new file mode 100644 index 0000000..f7704bd --- /dev/null +++ b/kvdb/kvdb.go @@ -0,0 +1,131 @@ +package kvdb + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" +) + +type KVDB struct { + Prefix string + Ext string +} + +func (kv *KVDB) Load( + keyif interface{}, + typ ...interface{}, +) (value interface{}, ok bool, err error) { + key, _ := keyif.(string) + if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#!:| \n") { + return nil, false, nil + } + + userFile := filepath.Join(kv.Prefix, key+"."+kv.Ext) + fmt.Println("Debug user file:", userFile) + b, err := ioutil.ReadFile(userFile) + if nil != err { + if os.IsNotExist(err) { + return nil, false, nil + } + fmt.Println("kvdb debug read:", err) + return nil, false, errors.New("database read failed") + } + + ok = true + value = b + if 1 == len(typ) { + err := json.Unmarshal(b, typ[0]) + if nil != err { + return nil, false, err + } + value = typ[0] + } else if len(b) > 0 && '"' == b[0] { + var str string + err := json.Unmarshal(b, &str) + if nil == err { + value = str + } + } + + return value, ok, nil +} + +func (kv *KVDB) Store(keyif interface{}, value interface{}) (err error) { + key, _ := keyif.(string) + if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") { + return errors.New("invalid key name") + } + + keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext) + f, err := os.Open(keypath) + if nil == err { + s, err := f.Stat() + if nil != err { + // if we can open, we should be able to stat + return errors.New("database connection failure") + } + ts := strconv.FormatInt(s.ModTime().Unix(), 10) + bakpath := filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext) + if err := os.Rename(keypath, bakpath); nil != err { + // keep the old record as a backup + return errors.New("database write failure") + } + } + + var b []byte + switch v := value.(type) { + case []byte: + b = v + case string: + b, _ = json.Marshal(v) + default: + fmt.Println("kvdb: not []byte or string:", v) + jsonb, err := json.Marshal(v) + if nil != err { + return err + } + b = jsonb + } + + if err := ioutil.WriteFile( + keypath, + b, + os.FileMode(0600), + ); nil != err { + fmt.Println("write failure:", err) + return errors.New("database write failed") + } + + return nil +} + +func (kv *KVDB) Delete(keyif interface{}) (err error) { + key, _ := keyif.(string) + if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") { + return errors.New("invalid key name") + } + + keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext) + f, err := os.Open(keypath) + if nil == err { + s, err := f.Stat() + if nil != err { + return errors.New("database connection failure") + } + ts := strconv.FormatInt(s.ModTime().Unix(), 64) + if err := os.Rename(keypath, filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext)); nil != err { + return errors.New("database connection failure") + } + } + + return nil +} + +func (kv *KVDB) Vacuum() (err error) { + return nil +} diff --git a/kvdb/kvdb_test.go b/kvdb/kvdb_test.go new file mode 100644 index 0000000..5328bd3 --- /dev/null +++ b/kvdb/kvdb_test.go @@ -0,0 +1,63 @@ +package kvdb + +import ( + "strings" + "testing" +) + +type TestEntry struct { + Email string `json:"email"` + Subjects []string `json:"subjects"` +} + +var email = "john@example.com" +var sub = "id123" +var dbPrefix = "../testdb" +var testKV = &KVDB{ + Prefix: dbPrefix + "/test-entries", + Ext: "eml.json", +} + +func TestStore(t *testing.T) { + entry := &TestEntry{ + Email: email, + Subjects: []string{sub}, + } + + if err := testKV.Store(email, entry); nil != err { + t.Fatal(err) + return + } + + value, ok, err := testKV.Load(email, &(TestEntry{})) + if nil != err { + t.Fatal(err) + return + } + if !ok { + t.Fatal("test entry not found") + } + + v, ok := value.(*TestEntry) + if !ok { + t.Fatal("test entry not of type TestEntry") + } + + if email != v.Email || sub != strings.Join(v.Subjects, ",") { + t.Fatalf("value: %#v", v) + } +} + +func TestNoExist(t *testing.T) { + value, ok, err := testKV.Load("not"+email, &(TestEntry{})) + if nil != err { + t.Fatal(err) + return + } + if ok { + t.Fatal("found entry that doesn't exist") + } + if value != nil { + t.Fatal("had value for entry that doesn't exist") + } +}