aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2023-11-24 16:02:59 +0100
committerRobin Jarry <robin@jarry.cc>2023-12-30 15:42:09 +0100
commitbb4db4ea65c0776f3088709e6dc2510612445226 (patch)
treec607cf28add329da270eff6f7db7dff984136a91 /lib
parent69e73fd7560ec7846451ad6fd859f9286812c64f (diff)
downloadaerc-bb4db4ea65c0776f3088709e6dc2510612445226.tar.gz
pama: implement the persistent store for projects
Implement the persistent store for models.Project structs. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'lib')
-rw-r--r--lib/pama/store/store.go244
1 files changed, 244 insertions, 0 deletions
diff --git a/lib/pama/store/store.go b/lib/pama/store/store.go
new file mode 100644
index 00000000..8f93873d
--- /dev/null
+++ b/lib/pama/store/store.go
@@ -0,0 +1,244 @@
+package store
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path"
+ "strings"
+
+ "git.sr.ht/~rjarry/aerc/lib/pama/models"
+ "git.sr.ht/~rjarry/aerc/lib/xdg"
+ "git.sr.ht/~rjarry/aerc/log"
+ "github.com/syndtr/goleveldb/leveldb"
+)
+
+const (
+ keyPrefix = "project."
+)
+
+var (
+ // versTag should be incremented when the underyling data structure
+ // changes.
+ versTag = []byte("0001")
+ versTagKey = []byte("version.tag")
+ currentKey = []byte("current.project")
+)
+
+func createKey(name string) []byte {
+ return []byte(keyPrefix + name)
+}
+
+func parseKey(key []byte) string {
+ return strings.TrimPrefix(string(key), keyPrefix)
+}
+
+func isProjectKey(key []byte) bool {
+ return bytes.HasPrefix(key, []byte(keyPrefix))
+}
+
+func cacheDir() (string, error) {
+ dir, err := os.UserCacheDir()
+ if err != nil {
+ dir = xdg.ExpandHome("~/.cache")
+ }
+ return path.Join(dir, "aerc"), nil
+}
+
+func openStorage() (*leveldb.DB, error) {
+ cd, err := cacheDir()
+ if err != nil {
+ return nil, fmt.Errorf("Unable to find project store directory: %w", err)
+ }
+ p := path.Join(cd, "projects")
+
+ db, err := leveldb.OpenFile(p, nil)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to open project store: %w", err)
+ }
+
+ has, err := db.Has(versTagKey, nil)
+ if err != nil {
+ return nil, err
+ }
+ setTag := !has
+ if has {
+ vers, err := db.Get(versTagKey, nil)
+ if err != nil {
+ return nil, err
+ }
+ if !bytes.Equal(vers, versTag) {
+ log.Warnf("patch store: version mismatch: wipe data")
+ iter := db.NewIterator(nil, nil)
+ for iter.Next() {
+ err := db.Delete(iter.Key(), nil)
+ if err != nil {
+ log.Errorf("delete: %v")
+ }
+ }
+ iter.Release()
+ setTag = true
+ }
+ }
+
+ if setTag {
+ err := db.Put(versTagKey, versTag, nil)
+ if err != nil {
+ return nil, err
+ }
+ log.Infof("patch store: set version: %s", string(versTag))
+ }
+
+ return db, nil
+}
+
+func encode(p models.Project) ([]byte, error) {
+ return json.Marshal(p)
+}
+
+func decode(data []byte) (p models.Project, err error) {
+ err = json.Unmarshal(data, &p)
+ return
+}
+
+func Store() models.PersistentStorer {
+ return &instance{}
+}
+
+type instance struct{}
+
+func (instance) StoreProject(p models.Project, overwrite bool) error {
+ db, err := openStorage()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ key := createKey(p.Name)
+ has, err := db.Has(key, nil)
+ if err != nil {
+ return err
+ }
+ if has && !overwrite {
+ return fmt.Errorf("Project '%s' already exists.", p.Name)
+ }
+
+ log.Debugf("project data: %v", p)
+
+ encoded, err := encode(p)
+ if err != nil {
+ return err
+ }
+ return db.Put(key, encoded, nil)
+}
+
+func (instance) DeleteProject(name string) error {
+ db, err := openStorage()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ key := createKey(name)
+ has, err := db.Has(key, nil)
+ if err != nil {
+ return err
+ }
+ if !has {
+ return fmt.Errorf("Project does not exist.")
+ }
+ return db.Delete(key, nil)
+}
+
+func (instance) CurrentName() (string, error) {
+ db, err := openStorage()
+ if err != nil {
+ return "", err
+ }
+ defer db.Close()
+ cur, err := db.Get(currentKey, nil)
+ if err != nil {
+ return "", err
+ }
+ return parseKey(cur), nil
+}
+
+func (instance) SetCurrent(name string) error {
+ db, err := openStorage()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+ key := createKey(name)
+ return db.Put(currentKey, key, nil)
+}
+
+func (instance) Current() (models.Project, error) {
+ db, err := openStorage()
+ if err != nil {
+ return models.Project{}, err
+ }
+ defer db.Close()
+
+ has, err := db.Has(currentKey, nil)
+ if err != nil {
+ return models.Project{}, err
+ }
+ if !has {
+ return models.Project{}, fmt.Errorf("No (current) project found; run 'project init' first.")
+ }
+ curProjectKey, err := db.Get(currentKey, nil)
+ if err != nil {
+ return models.Project{}, err
+ }
+ raw, err := db.Get(curProjectKey, nil)
+ if err != nil {
+ return models.Project{}, err
+ }
+ p, err := decode(raw)
+ if err != nil {
+ return models.Project{}, err
+ }
+ return p, nil
+}
+
+func (instance) Names() ([]string, error) {
+ db, err := openStorage()
+ if err != nil {
+ return nil, err
+ }
+ defer db.Close()
+ var names []string
+ iter := db.NewIterator(nil, nil)
+ for iter.Next() {
+ if !isProjectKey(iter.Key()) {
+ continue
+ }
+ names = append(names, parseKey(iter.Key()))
+ }
+ iter.Release()
+ return names, nil
+}
+
+func (instance) Projects() ([]models.Project, error) {
+ var projects []models.Project
+ db, err := openStorage()
+ if err != nil {
+ return nil, err
+ }
+ defer db.Close()
+ iter := db.NewIterator(nil, nil)
+ for iter.Next() {
+ if !isProjectKey(iter.Key()) {
+ continue
+ }
+ p, err := decode(iter.Value())
+ if err != nil {
+ return nil, err
+ }
+ projects = append(projects, p)
+ }
+ iter.Release()
+ return projects, nil
+}