|
|
@ -1,3 +1,9 @@ |
|
|
|
// 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.
|
|
|
|
package jsondb |
|
|
|
|
|
|
|
import ( |
|
|
@ -8,16 +14,20 @@ import ( |
|
|
|
"fmt" |
|
|
|
"net/url" |
|
|
|
"os" |
|
|
|
"path/filepath" |
|
|
|
"strings" |
|
|
|
"sync" |
|
|
|
"time" |
|
|
|
|
|
|
|
again "git.rootprojects.org/root/go-again" |
|
|
|
"git.rootprojects.org/root/go-again" |
|
|
|
) |
|
|
|
|
|
|
|
type JSONDB struct { |
|
|
|
dburl string |
|
|
|
path string |
|
|
|
json *dbjson |
|
|
|
mux sync.Mutex |
|
|
|
fmux sync.Mutex |
|
|
|
} |
|
|
|
|
|
|
|
type dbjson struct { |
|
|
@ -31,13 +41,9 @@ func Connect(dburl string) (*JSONDB, error) { |
|
|
|
} |
|
|
|
|
|
|
|
// json:/abspath/to/db.json
|
|
|
|
fmt.Println("url.Opaque:", u.Opaque) |
|
|
|
// json:///abspath/to/db.json
|
|
|
|
fmt.Println("url.Path:", u.Path) |
|
|
|
fmt.Println(u) |
|
|
|
|
|
|
|
path := u.Opaque |
|
|
|
if "" == path { |
|
|
|
// json:///abspath/to/db.json
|
|
|
|
path = u.Path |
|
|
|
if "" == path { |
|
|
|
// json:relpath/to/db.json
|
|
|
@ -73,10 +79,14 @@ func Connect(dburl string) (*JSONDB, error) { |
|
|
|
return nil, fmt.Errorf("Couldn't parse %q as JSON: %s", path, err) |
|
|
|
} |
|
|
|
|
|
|
|
wd, _ := os.Getwd() |
|
|
|
fmt.Println("jsondb:", filepath.Join(wd, path)) |
|
|
|
return &JSONDB{ |
|
|
|
dburl: dburl, |
|
|
|
path: path, |
|
|
|
json: db, |
|
|
|
mux: sync.Mutex{}, |
|
|
|
fmux: sync.Mutex{}, |
|
|
|
}, nil |
|
|
|
} |
|
|
|
|
|
|
@ -92,10 +102,6 @@ type Schedule struct { |
|
|
|
Webhooks []again.Webhook `json:"webhooks" db"webhooks"` |
|
|
|
} |
|
|
|
|
|
|
|
func ctcmp(x string, y string) bool { |
|
|
|
return 1 == subtle.ConstantTimeCompare([]byte(x), []byte(y)) |
|
|
|
} |
|
|
|
|
|
|
|
func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) { |
|
|
|
schedules := []*again.Schedule{} |
|
|
|
for i := range db.json.Schedules { |
|
|
@ -115,30 +121,11 @@ func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) { |
|
|
|
return schedules, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (db *JSONDB) get(id string) (int, *Schedule) { |
|
|
|
for i := range db.json.Schedules { |
|
|
|
schedule := db.json.Schedules[i] |
|
|
|
if ctcmp(id, schedule.ID) { |
|
|
|
return i, &schedule |
|
|
|
} |
|
|
|
} |
|
|
|
return -1, nil |
|
|
|
} |
|
|
|
|
|
|
|
func genID() (string, error) { |
|
|
|
b := make([]byte, 16) |
|
|
|
_, err := rand.Read(b) |
|
|
|
if nil != err { |
|
|
|
return "", err |
|
|
|
} |
|
|
|
return hex.EncodeToString(b), nil |
|
|
|
} |
|
|
|
|
|
|
|
func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) { |
|
|
|
exists := false |
|
|
|
index := -1 |
|
|
|
if "" == s.ID { |
|
|
|
id, err := genID() |
|
|
|
id, err := genID(16) |
|
|
|
if nil != err { |
|
|
|
return nil, err |
|
|
|
} |
|
|
@ -147,6 +134,7 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) { |
|
|
|
i, old := db.get(s.ID) |
|
|
|
index = i |
|
|
|
exists = nil != old |
|
|
|
// TODO constant time bail
|
|
|
|
if !exists || !ctcmp(old.AccessID, s.AccessID) { |
|
|
|
return nil, fmt.Errorf("invalid id") |
|
|
|
} |
|
|
@ -163,9 +151,13 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) { |
|
|
|
} |
|
|
|
|
|
|
|
if exists { |
|
|
|
db.mux.Lock() |
|
|
|
db.json.Schedules[index] = schedule |
|
|
|
db.mux.Unlock() |
|
|
|
} else { |
|
|
|
db.mux.Lock() |
|
|
|
db.json.Schedules = append(db.json.Schedules, schedule) |
|
|
|
db.mux.Unlock() |
|
|
|
} |
|
|
|
|
|
|
|
err := db.save(s.AccessID) |
|
|
@ -176,9 +168,73 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) { |
|
|
|
return &s, nil |
|
|
|
} |
|
|
|
|
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
func (db *JSONDB) save(accessID string) error { |
|
|
|
// TODO per-user files (w/ mutex lock or channel on open and write)
|
|
|
|
tmppath := db.path + ".tmp" |
|
|
|
// 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" |
|
|
|
bakpath := db.path + ".bak" |
|
|
|
|
|
|
|
os.Remove(tmppath) // ignore error
|
|
|
@ -194,12 +250,15 @@ func (db *JSONDB) save(accessID string) error { |
|
|
|
return err |
|
|
|
} |
|
|
|
|
|
|
|
// TODO could make async and debounce...
|
|
|
|
// or spend that time on something useful
|
|
|
|
db.fmux.Lock() |
|
|
|
defer db.fmux.Unlock() |
|
|
|
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 |
|
|
|