go-jamming/db/repo.go

185 lines
4.9 KiB
Go

// A database wrapper package for BuntDB that persists indieweb (meta)data.
// Most functions silently suppress errors as with consistent states, it would be impossible.
package db
import (
"brainbaking.com/go-jamming/app/mf"
"brainbaking.com/go-jamming/common"
"encoding/json"
"fmt"
"github.com/rs/zerolog/log"
"github.com/tidwall/buntdb"
"time"
)
type MentionRepoBunt struct {
db *buntdb.DB
}
type MentionRepo interface {
Save(key mf.Mention, data *mf.IndiewebData) (string, error)
SavePicture(bytes string, domain string) (string, error)
Delete(key mf.Mention)
Since(domain string) (time.Time, error)
UpdateSince(domain string, since time.Time)
Get(key mf.Mention) *mf.IndiewebData
GetPicture(domain string) []byte
GetAll(domain string) mf.IndiewebDataResult
}
// UpdateSince updates the since timestamp to now. Logs but ignores errors.
func (r *MentionRepoBunt) UpdateSince(domain string, since time.Time) {
err := r.db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(sinceKey(domain), common.TimeToIso(since), nil)
return err
})
if err != nil {
log.Error().Err(err).Msg("UpdateSince: unable to save")
}
}
// Since fetches the last timestamp of the mf send.
// Returns converted found instance, or an error if none found.
func (r *MentionRepoBunt) Since(domain string) (time.Time, error) {
var since string
err := r.db.View(func(tx *buntdb.Tx) error {
val, err := tx.Get(sinceKey(domain))
since = val
return err
})
if err != nil {
return time.Time{}, err
}
return common.IsoToTime(since), nil
}
func sinceKey(domain string) string {
return fmt.Sprintf("%s:since", domain)
}
// Delete removes a possibly present mention by key. Ignores possible errors.
func (r *MentionRepoBunt) Delete(wm mf.Mention) {
key := r.mentionToKey(wm)
r.db.Update(func(tx *buntdb.Tx) error {
tx.Delete(key)
return nil
})
}
func (r *MentionRepoBunt) SavePicture(bytes string, domain string) (string, error) {
key := pictureKey(domain)
err := r.db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(key, bytes, nil)
return err
})
if err != nil {
return "", err
}
return key, nil
}
func pictureKey(domain string) string {
return fmt.Sprintf("%s:picture", domain)
}
// Save saves the mention by marshalling data. Returns the key or a marshal/persist error.
func (r *MentionRepoBunt) Save(wm mf.Mention, data *mf.IndiewebData) (string, error) {
jsonData, err := json.Marshal(data)
if err != nil {
return "", err
}
key := r.mentionToKey(wm)
err = r.db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(key, string(jsonData), nil)
return err
})
if err != nil {
return "", err
}
return key, nil
}
func (r *MentionRepoBunt) mentionToKey(wm mf.Mention) string {
return fmt.Sprintf("%s:%s", wm.Key(), wm.Domain())
}
// Get returns a single unmarshalled json value based on the mention key.
// It returns the unmarshalled result or nil if something went wrong.
func (r *MentionRepoBunt) Get(wm mf.Mention) *mf.IndiewebData {
var data mf.IndiewebData
key := r.mentionToKey(wm)
err := r.db.View(func(tx *buntdb.Tx) error {
val, err := tx.Get(key)
if err != nil {
return err
}
err = json.Unmarshal([]byte(val), &data)
if err != nil {
return err
}
return nil
})
if err != nil {
log.Error().Err(err).Str("key", key).Msg("repo get: unable to retrieve key")
return nil
}
return &data
}
func (r *MentionRepoBunt) GetPicture(domain string) []byte {
var data []byte
key := pictureKey(domain)
err := r.db.View(func(tx *buntdb.Tx) error {
val, err := tx.Get(key)
if err != nil {
return err
}
data = []byte(val)
return nil
})
if err != nil {
log.Error().Err(err).Str("key", key).Msg("repo getpicture: unable to retrieve key")
return nil
}
return data
}
// GetAll returns a wrapped data result for all mentions for a particular domain.
// Intentionally ignores marshal errors, db should be consistent!
// Warning, this will potentially marshall 10k strings!
func (r *MentionRepoBunt) GetAll(domain string) mf.IndiewebDataResult {
var data []*mf.IndiewebData
err := r.db.View(func(tx *buntdb.Tx) error {
return tx.Ascend(domain, func(key, value string) bool {
var result mf.IndiewebData
json.Unmarshal([]byte(value), &result)
data = append(data, &result)
return true
})
})
if err != nil {
log.Error().Err(err).Msg("get all: failed to ascend from view")
return mf.IndiewebDataResult{}
}
return mf.ResultSuccess(data)
}
// NewMentionRepo opens a database connection using default buntdb settings.
// It also creates necessary indexes based on the passed domain config.
// This panics if it cannot open the db.
func NewMentionRepo(c *common.Config) *MentionRepoBunt {
repo := &MentionRepoBunt{}
db, err := buntdb.Open(c.ConString)
if err != nil {
log.Fatal().Str("constr", c.ConString).Msg("new mention repo: cannot open db")
}
repo.db = db
for _, domain := range c.AllowedWebmentionSources {
db.CreateIndex(domain, fmt.Sprintf("*:%s", domain), buntdb.IndexString)
}
return repo
}