go-again/data/jsondb/jsondb.go

291 lines
6.2 KiB
Go
Raw Permalink Normal View History

2019-06-23 09:01:24 +00:00
// A JSON storage strategy for Go Again
// SQL would be a better choice, but... meh
//
// Note that we use mutexes instead of channels
// because everything is both synchronous and
// sequential. Meh.
2019-06-21 18:35:08 +00:00
package jsondb
import (
2019-06-23 06:08:05 +00:00
"crypto/rand"
"crypto/subtle"
"encoding/hex"
2019-06-22 07:10:21 +00:00
"encoding/json"
"fmt"
"net/url"
"os"
2019-06-23 09:01:24 +00:00
"path/filepath"
2019-06-24 01:02:31 +00:00
"sort"
2019-06-22 07:10:21 +00:00
"strings"
2019-06-23 09:01:24 +00:00
"sync"
2019-06-23 03:50:17 +00:00
"time"
2019-06-21 18:35:08 +00:00
2019-06-23 09:01:24 +00:00
"git.rootprojects.org/root/go-again"
2019-06-21 18:35:08 +00:00
)
2019-06-22 07:10:21 +00:00
type JSONDB struct {
dburl string
2019-06-23 03:50:17 +00:00
path string
2019-06-22 07:10:21 +00:00
json *dbjson
2019-06-23 09:01:24 +00:00
mux sync.Mutex
fmux sync.Mutex
2019-06-22 07:10:21 +00:00
}
type dbjson struct {
2019-06-23 03:50:17 +00:00
Schedules []Schedule `json:"schedules"`
2019-06-22 07:10:21 +00:00
}
func Connect(dburl string) (*JSONDB, error) {
u, err := url.Parse(dburl)
if nil != err {
return nil, err
}
// json:/abspath/to/db.json
path := u.Opaque
if "" == path {
2019-06-23 09:01:24 +00:00
// json:///abspath/to/db.json
2019-06-22 07:10:21 +00:00
path = u.Path
if "" == path {
// json:relpath/to/db.json
// json://relpath/to/db.json
path = strings.TrimSuffix(u.Host+"/"+u.Path, "/")
}
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0700)
if nil != err {
return nil, fmt.Errorf("Couldn't open %q: %s", path, err)
}
stat, err := f.Stat()
if 0 == stat.Size() {
_, err := f.Write([]byte(`{"schedules":[]}`))
2019-06-23 03:50:17 +00:00
f.Close()
2019-06-22 07:10:21 +00:00
if nil != err {
return nil, err
}
2019-06-23 03:50:17 +00:00
2019-06-22 07:10:21 +00:00
f, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0700)
if nil != err {
return nil, err
}
}
decoder := json.NewDecoder(f)
db := &dbjson{}
err = decoder.Decode(db)
2019-06-23 03:50:17 +00:00
f.Close()
2019-06-22 07:10:21 +00:00
if nil != err {
return nil, fmt.Errorf("Couldn't parse %q as JSON: %s", path, err)
}
2019-06-23 09:01:24 +00:00
wd, _ := os.Getwd()
fmt.Println("jsondb:", filepath.Join(wd, path))
2019-06-22 07:10:21 +00:00
return &JSONDB{
dburl: dburl,
2019-06-23 03:50:17 +00:00
path: path,
2019-06-22 07:10:21 +00:00
json: db,
2019-06-23 09:01:24 +00:00
mux: sync.Mutex{},
fmux: sync.Mutex{},
2019-06-22 07:10:21 +00:00
}, nil
}
2019-06-23 03:50:17 +00:00
// A copy of again.Schedule, but with access_id json-able
type Schedule struct {
ID string `json:"id" db:"id"`
AccessID string `json:"access_id" db:"access_id"`
Date string `json:"date" db:"date"`
Time string `json:"time" db:"time"`
TZ string `json:"tz" db:"tz"`
NextRunAt time.Time `json:"next_run_at" db:"next_run_at"`
Disabled bool `json:"disabled" db:"disabled"`
Webhooks []again.Webhook `json:"webhooks" db"webhooks"`
}
func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) {
2019-06-24 01:02:31 +00:00
nowish := time.Now().Add(time.Duration(30) * time.Second)
2019-06-23 03:50:17 +00:00
schedules := []*again.Schedule{}
for i := range db.json.Schedules {
s := db.json.Schedules[i]
2019-06-24 01:02:31 +00:00
if !s.Disabled && ctcmp(accessID, s.AccessID) && s.NextRunAt.Sub(nowish) > 0 {
2019-06-23 03:50:17 +00:00
schedules = append(schedules, &again.Schedule{
ID: s.ID,
AccessID: s.AccessID,
Date: s.Date,
Time: s.Time,
TZ: s.TZ,
NextRunAt: s.NextRunAt,
Webhooks: s.Webhooks,
})
}
}
return schedules, nil
}
2019-06-23 06:08:05 +00:00
func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
exists := false
index := -1
if "" == s.ID {
2019-06-23 09:01:24 +00:00
id, err := genID(16)
2019-06-23 06:08:05 +00:00
if nil != err {
return nil, err
}
s.ID = id
} else {
i, old := db.get(s.ID)
index = i
exists = nil != old
2019-06-23 09:01:24 +00:00
// TODO constant time bail
2019-06-23 06:08:05 +00:00
if !exists || !ctcmp(old.AccessID, s.AccessID) {
return nil, fmt.Errorf("invalid id")
2019-06-23 03:50:17 +00:00
}
}
schedule := Schedule{
ID: s.ID,
AccessID: s.AccessID,
Date: s.Date,
Time: s.Time,
TZ: s.TZ,
NextRunAt: s.NextRunAt,
Webhooks: s.Webhooks,
}
2019-06-23 06:08:05 +00:00
if exists {
2019-06-23 09:01:24 +00:00
db.mux.Lock()
2019-06-23 06:08:05 +00:00
db.json.Schedules[index] = schedule
2019-06-23 09:01:24 +00:00
db.mux.Unlock()
2019-06-23 06:08:05 +00:00
} else {
2019-06-23 09:01:24 +00:00
db.mux.Lock()
2019-06-23 06:08:05 +00:00
db.json.Schedules = append(db.json.Schedules, schedule)
2019-06-23 09:01:24 +00:00
db.mux.Unlock()
2019-06-23 06:08:05 +00:00
}
2019-06-23 03:50:17 +00:00
err := db.save(s.AccessID)
if nil != err {
return nil, err
}
return &s, nil
}
2019-06-23 09:01:24 +00:00
func (db *JSONDB) Delete(accessID string, id string) (*again.Schedule, error) {
_, old := db.get(id)
exists := nil != old
// TODO constant time bail
if !exists || !ctcmp(old.AccessID, accessID) {
return nil, fmt.Errorf("invalid id")
}
// Copy everything we keep into its own array
newSchedules := []Schedule{}
for i := range db.json.Schedules {
schedule := db.json.Schedules[i]
if old.ID != schedule.ID {
newSchedules = append(newSchedules, schedule)
}
}
db.mux.Lock()
db.json.Schedules = newSchedules
db.mux.Unlock()
err := db.save(accessID)
if nil != err {
return nil, err
}
return &again.Schedule{
ID: old.ID,
AccessID: old.AccessID,
Date: old.Date,
Time: old.Time,
TZ: old.TZ,
NextRunAt: old.NextRunAt,
Webhooks: old.Webhooks,
}, nil
}
2019-06-24 01:02:31 +00:00
func (db *JSONDB) Upcoming(min time.Time, max time.Time) ([]*again.Schedule, error) {
schedules := []*again.Schedule{}
for i := range db.json.Schedules {
s := db.json.Schedules[i]
if !s.Disabled && s.NextRunAt.Sub(min) > 0 && max.Sub(s.NextRunAt) > 0 {
schedules = append(schedules, &again.Schedule{
ID: s.ID,
AccessID: s.AccessID,
Date: s.Date,
Time: s.Time,
TZ: s.TZ,
NextRunAt: s.NextRunAt,
Webhooks: s.Webhooks,
})
}
}
sort.Sort(again.Schedules(schedules))
return []*again.Schedule(schedules), nil
}
2019-06-23 09:01:24 +00:00
func ctcmp(x string, y string) bool {
return 1 == subtle.ConstantTimeCompare([]byte(x), []byte(y))
}
func (db *JSONDB) get(id string) (int, *Schedule) {
db.mux.Lock()
scheds := db.json.Schedules
db.mux.Unlock()
for i := range scheds {
schedule := scheds[i]
if ctcmp(id, schedule.ID) {
return i, &schedule
}
}
return -1, nil
}
func genID(n int) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if nil != err {
return "", err
}
return hex.EncodeToString(b), nil
}
2019-06-23 03:50:17 +00:00
func (db *JSONDB) save(accessID string) error {
2019-06-23 09:01:24 +00:00
// TODO per-user files, maybe
// or probably better to spend that time building the postgres adapter
rnd, err := genID(4)
tmppath := db.path + "." + rnd + ".tmp"
2019-06-23 06:08:05 +00:00
bakpath := db.path + ".bak"
os.Remove(tmppath) // ignore error
f, err := os.OpenFile(tmppath, os.O_RDWR|os.O_CREATE, 0700)
2019-06-23 03:50:17 +00:00
if nil != err {
return err
}
encoder := json.NewEncoder(f)
2019-06-23 06:08:05 +00:00
err = encoder.Encode(db.json)
2019-06-23 03:50:17 +00:00
f.Close()
if nil != err {
return err
}
2019-06-23 09:01:24 +00:00
// TODO could make async and debounce...
// or spend that time on something useful
db.fmux.Lock()
defer db.fmux.Unlock()
2019-06-23 06:08:05 +00:00
os.Remove(bakpath) // ignore error
err = os.Rename(db.path, bakpath)
if nil != err {
return err
}
err = os.Rename(tmppath, db.path)
if nil != err {
return err
}
2019-06-23 03:50:17 +00:00
return nil
2019-06-21 18:35:08 +00:00
}