package store import ( "bytes" "encoding/json" "fmt" "os" "path" "strings" "git.sr.ht/~rjarry/aerc/lib/log" "git.sr.ht/~rjarry/aerc/lib/pama/models" "git.sr.ht/~rjarry/aerc/lib/xdg" "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) Project(name string) (models.Project, error) { db, err := openStorage() if err != nil { return models.Project{}, err } defer db.Close() raw, err := db.Get(createKey(name), nil) if err != nil { return models.Project{}, err } p, err := decode(raw) if err != nil { return models.Project{}, err } return p, 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 }