diff --git a/boltrest.go b/boltrest.go new file mode 100644 index 0000000..2b04491 --- /dev/null +++ b/boltrest.go @@ -0,0 +1,164 @@ +package boltrest + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "github.com/boltdb/bolt" +) + +// This a custom library for saving/syncing data +// in a local bolt db and a remote db through a +// rest api. + +// Custom BoltRest DB keys: +// __BR_LastUpdate: + +// DB is a struct for accomplishing this +type DB struct { + localFile string + remoteURL string + apiToken string + remoteMD5 string + online bool + localDB *bolt.DB +} + +// Open returns the DB object +func Open(filename string) (*DB, error) { + var err error + b := DB{localFile: filename} + b.localDB, err = bolt.Open(filename, 0644, nil) + if err != nil { + return nil, err + } + return &b, nil +} + +// GetValue returns the value at path +// path is a '/' separated list of tokens +// the last token is a key, all others are buckets +func (b *DB) GetValue(path []string, key string) (string, error) { + var err error + var ret string + // TODO: Make sure local db is fresh (or offline) + + b.localDB.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(path[0])) + if bkt == nil { + return fmt.Errorf("Couldn't find bucket " + path[0]) + } + var newBkt *bolt.Bucket + for idx := 1; idx < len(path); idx++ { + newBkt = bkt.Bucket([]byte(path[idx])) + if newBkt == nil { + return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], "/")) + } + } + // newBkt should have the last bucket in the path + ret = string(newBkt.Get([]byte(key))) + return nil + }) + return ret, err +} + +// SetValue sets the value at path to val +// path is a '/' separated list of tokens +// the last token is a key, all others are buckets +func (b *DB) SetValue(path []string, key, val string) error { + var err error + b.localDB.Update(func(tx *bolt.Tx) error { + newBkt := tx.Bucket([]byte(path[0])) + if newBkt == nil { + return fmt.Errorf("Couldn't find bucket " + path[0]) + } + for idx := 1; idx < len(path); idx++ { + newBkt, err = newBkt.CreateBucketIfNotExists([]byte(path[idx])) + if err != nil { + return err + } + } + // newBkt should have the last bucket in the path + return newBkt.Put([]byte(key), []byte(val)) + }) + return err +} + +// GetInt returns the value at 'path' +// path is a '/' separated list of tokens +// the last token is a key, all others are buckets +// If the value cannot be parsed as an int, error +func (b *DB) GetInt(path []string, key string) (int, error) { + var ret int + r, err := b.GetValue(path, key) + if err == nil { + ret, err = strconv.Atoi(r) + } + return ret, err +} + +// SetInt Sets an integer value +func (b *DB) SetInt(path []string, key string, val int) error { + return b.SetValue(path, key, strconv.Itoa(val)) +} + +// GetBool returns the value at 'path' +// If the value cannot be parsed as a bool, error +// We check 'true/false' and '1/0', else error +func (b *DB) GetBool(path []string, key string) (bool, error) { + var ret bool + r, err := b.GetValue(path, key) + if err == nil { + if r == "true" || r == "1" { + ret = true + } else if r != "false" && r != "0" { + err = fmt.Errorf("Cannot parse as a boolean") + } + } + return ret, err +} + +// SetBool Sets a boolean value +func (b *DB) SetBool(path []string, key string, val bool) error { + if val { + return b.SetValue(path, key, "true") + } + return b.SetValue(path, key, "false") +} + +// GetTimestamp returns the value at 'path' +// If the value cannot be parsed as a RFC3339, error +func (b *DB) GetTimestamp(path []string, key string) (time.Time, error) { + r, err := b.GetValue(path, key) + if err == nil { + return time.Parse(time.RFC3339, r) + } + return time.Unix(0, 0), err +} + +// SetTimestamp returns the value at 'path' +func (b *DB) SetTimestamp(path []string, key string, val time.Time) error { + return b.SetValue(path, key, val.Format(time.RFC3339)) +} + +type setURLResponse struct { + dbMD5 string +} + +// SetURL sets the DB's remote URL +func (b *DB) SetURL(url string) (bool, string) { + resp, err := http.Get(url) + if err != nil { + return false, "" + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + sur := setURLResponse{} + json.Unmarshal(body, sur) + return true, sur.dbMD5 +}