WIP delete works too now

This commit is contained in:
AJ ONeal 2019-06-23 03:01:24 -06:00
parent 9db30c7e80
commit d87b197cc0
5 changed files with 225 additions and 128 deletions

View File

@ -208,21 +208,21 @@ func IsAmbiguous(st []int, tzstr string) error {
if nil != err { if nil != err {
return err return err
} }
m := time.Month(st[1]) m := time.Month(st[1])
t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], st[6], tz) t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], st[6], tz)
u1 := t1.UTC() u1 := t1.UTC()
// A better way to do this would probably be to parse the timezone database, but... yeah... // Australia/Lord_Howe has a 30-minute DST
for _, n := range []int{ /*-120, -60,*/ 30, 60, 120} { // 60-minute DST is common
// Antarctica/Troll has a 120-minute DST
for _, n := range []int{30, 60, 120} {
t2 := time.Date(st[0], m, st[2], st[3], st[4]+n, st[5], st[6], tz) t2 := time.Date(st[0], m, st[2], st[3], st[4]+n, st[5], st[6], tz)
u2 := t2.UTC() u2 := t2.UTC()
if u1.Equal(u2) { if u1.Equal(u2) {
fmt.Println("Ambiguous Time") return fmt.Errorf("Ambiguous: %s, %s, %+d\n", t1, t2, n)
fmt.Printf("%s, %s, %+d\n", t1, u1, n)
fmt.Printf("%s, %s, %+d\n", t2, u2, n)
return fmt.Errorf("Ambiguous")
} }
} }
//ta :=
return nil return nil
} }

View File

@ -77,6 +77,7 @@ func main() {
} }
//mux.Handle("/api/", http.HandlerFunc(handleFunc)) //mux.Handle("/api/", http.HandlerFunc(handleFunc))
mux.HandleFunc("/api/v0/schedules", s.Handle) mux.HandleFunc("/api/v0/schedules", s.Handle)
mux.HandleFunc("/api/v0/schedules/", s.Handle)
// TODO Filebox FS // TODO Filebox FS
mux.Handle("/", http.FileServer(http.Dir("./public"))) mux.Handle("/", http.FileServer(http.Dir("./public")))
@ -88,6 +89,7 @@ func main() {
type ScheduleDB interface { type ScheduleDB interface {
List(string) ([]*again.Schedule, error) List(string) ([]*again.Schedule, error)
Set(again.Schedule) (*again.Schedule, error) Set(again.Schedule) (*again.Schedule, error)
Delete(accessID string, id string) (*again.Schedule, error)
} }
type scheduler struct { type scheduler struct {
@ -107,7 +109,6 @@ func (s *scheduler) Handle(w http.ResponseWriter, r *http.Request) {
ctx = context.WithValue(ctx, "token", token) ctx = context.WithValue(ctx, "token", token)
r = r.WithContext(ctx) r = r.WithContext(ctx)
fmt.Println("whatever", r.Method, r.URL)
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
s.List(w, r) s.List(w, r)
@ -115,6 +116,9 @@ func (s *scheduler) Handle(w http.ResponseWriter, r *http.Request) {
case http.MethodPost: case http.MethodPost:
s.Create(w, r) s.Create(w, r)
return return
case http.MethodDelete:
s.Delete(w, r)
return
default: default:
http.Error(w, "Not Implemented", http.StatusNotImplemented) http.Error(w, "Not Implemented", http.StatusNotImplemented)
return return
@ -144,10 +148,8 @@ func (s *scheduler) Create(w http.ResponseWriter, r *http.Request) {
br, bw := io.Pipe() br, bw := io.Pipe()
b := io.TeeReader(r.Body, bw) b := io.TeeReader(r.Body, bw)
go func() { go func() {
fmt.Println("reading from reader...")
x, _ := ioutil.ReadAll(b) x, _ := ioutil.ReadAll(b)
fmt.Println("cool beans and all") fmt.Println("[debug] http body", string(x))
fmt.Println(string(x))
bw.Close() bw.Close()
}() }()
decoder := json.NewDecoder(br) decoder := json.NewDecoder(br)
@ -176,3 +178,29 @@ func (s *scheduler) Create(w http.ResponseWriter, r *http.Request) {
} }
w.Write(buf) w.Write(buf)
} }
func (s *scheduler) Delete(w http.ResponseWriter, r *http.Request) {
// TODO validate user
accessID := r.Context().Value("token").(string)
parts := strings.Split(r.URL.Path, "/")
// ""/"api"/"v0"/"schedules"/":id"
if 5 != len(parts) {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
id := parts[4]
sched2, err := s.DB.Delete(accessID, id)
if nil != err {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf, err := json.Marshal(sched2)
if nil != err {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(buf)
}

View File

@ -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 package jsondb
import ( import (
@ -8,16 +14,20 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"sync"
"time" "time"
again "git.rootprojects.org/root/go-again" "git.rootprojects.org/root/go-again"
) )
type JSONDB struct { type JSONDB struct {
dburl string dburl string
path string path string
json *dbjson json *dbjson
mux sync.Mutex
fmux sync.Mutex
} }
type dbjson struct { type dbjson struct {
@ -31,13 +41,9 @@ func Connect(dburl string) (*JSONDB, error) {
} }
// json:/abspath/to/db.json // 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 path := u.Opaque
if "" == path { if "" == path {
// json:///abspath/to/db.json
path = u.Path path = u.Path
if "" == path { if "" == path {
// json:relpath/to/db.json // 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) 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{ return &JSONDB{
dburl: dburl, dburl: dburl,
path: path, path: path,
json: db, json: db,
mux: sync.Mutex{},
fmux: sync.Mutex{},
}, nil }, nil
} }
@ -92,10 +102,6 @@ type Schedule struct {
Webhooks []again.Webhook `json:"webhooks" db"webhooks"` 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) { func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) {
schedules := []*again.Schedule{} schedules := []*again.Schedule{}
for i := range db.json.Schedules { for i := range db.json.Schedules {
@ -115,30 +121,11 @@ func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) {
return schedules, nil 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) { func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
exists := false exists := false
index := -1 index := -1
if "" == s.ID { if "" == s.ID {
id, err := genID() id, err := genID(16)
if nil != err { if nil != err {
return 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) i, old := db.get(s.ID)
index = i index = i
exists = nil != old exists = nil != old
// TODO constant time bail
if !exists || !ctcmp(old.AccessID, s.AccessID) { if !exists || !ctcmp(old.AccessID, s.AccessID) {
return nil, fmt.Errorf("invalid id") return nil, fmt.Errorf("invalid id")
} }
@ -163,9 +151,13 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
} }
if exists { if exists {
db.mux.Lock()
db.json.Schedules[index] = schedule db.json.Schedules[index] = schedule
db.mux.Unlock()
} else { } else {
db.mux.Lock()
db.json.Schedules = append(db.json.Schedules, schedule) db.json.Schedules = append(db.json.Schedules, schedule)
db.mux.Unlock()
} }
err := db.save(s.AccessID) err := db.save(s.AccessID)
@ -176,9 +168,73 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
return &s, nil 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 { func (db *JSONDB) save(accessID string) error {
// TODO per-user files (w/ mutex lock or channel on open and write) // TODO per-user files, maybe
tmppath := db.path + ".tmp" // or probably better to spend that time building the postgres adapter
rnd, err := genID(4)
tmppath := db.path + "." + rnd + ".tmp"
bakpath := db.path + ".bak" bakpath := db.path + ".bak"
os.Remove(tmppath) // ignore error os.Remove(tmppath) // ignore error
@ -194,12 +250,15 @@ func (db *JSONDB) save(accessID string) error {
return err 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 os.Remove(bakpath) // ignore error
err = os.Rename(db.path, bakpath) err = os.Rename(db.path, bakpath)
if nil != err { if nil != err {
return err return err
} }
err = os.Rename(tmppath, db.path) err = os.Rename(tmppath, db.path)
if nil != err { if nil != err {
return err return err

View File

@ -50,8 +50,8 @@
newWebhookHeader(ev.target); newWebhookHeader(ev.target);
} else if (ev.target.matches('.js-rm-header')) { } else if (ev.target.matches('.js-rm-header')) {
rmWebhookHeader(ev.target); rmWebhookHeader(ev.target);
} else if (ev.target.matches('.js-delete') && ev.target.closest('.js-webhook')) { } else if (ev.target.matches('.js-delete') && ev.target.closest('.js-schedule')) {
deleteWebhook(ev.target.closest('.js-webhook')); deleteSchedule(ev.target.closest('.js-schedule'));
} else { } else {
return; return;
} }
@ -142,11 +142,12 @@
throw new Error('something bad happened'); throw new Error('something bad happened');
} }
state.account.schedules.push(resp.data); state.account.schedules.push(data);
displayAccount(state.account); displayAccount(state.account);
}) })
.catch(function(e) { .catch(function(e) {
console.error(e);
window.alert(e.message); window.alert(e.message);
}); });
}); });
@ -168,9 +169,8 @@
var $h = $rmHeader.closest('.js-header'); var $h = $rmHeader.closest('.js-header');
$h.parentElement.removeChild($h); $h.parentElement.removeChild($h);
} }
function deleteWebhook($hook) { function deleteSchedule($sched) {
var deviceId = $hook.closest('.js-schedule').querySelector('.js-id').value; var schedId = $('.js-id', $sched).value;
var id = $('.js-id', $hook).innerText;
var opts = { var opts = {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@ -179,27 +179,17 @@
}, },
cors: true cors: true
}; };
window.fetch('/api/iot/devices/' + deviceId + '/webhooks/' + id, opts).then(function(resp) { window.fetch('/api/v0/schedules/' + schedId, opts).then(function(resp) {
return resp.json().then(function(result) { return resp.json().then(function(result) {
if (!result.webhook) { if (!result.webhooks) {
console.error(result); console.error(result);
window.alert('something went wrong: ' + JSON.stringify(result)); window.alert('something went wrong: ' + JSON.stringify(result));
return; return;
} }
var index = -1; state.account.schedules = state.account.schedules.filter(function(g) {
var dev = state.account.devices.filter(function(d, i) { return g.id !== result.id;
return d.accessToken == deviceId;
})[0];
dev.webhooks.some(function(g, i) {
if (g.id === id) {
index = i;
return true;
}
}); });
if (index > -1) { displayAccount(state.account);
dev.webhooks.splice(index, 1);
displayAccount(state.account);
}
}); });
}); });
} }
@ -211,6 +201,7 @@
var $devs = $('.js-schedules'); var $devs = $('.js-schedules');
$devs.innerHTML = ''; $devs.innerHTML = '';
data.schedules.forEach(function(d) { data.schedules.forEach(function(d) {
console.log('schedule', d);
var $dev = $.create($devTpl); var $dev = $.create($devTpl);
$('.js-id', $dev).value = d.id; $('.js-id', $dev).value = d.id;
$('.js-date', $dev).value = d.date; $('.js-date', $dev).value = d.date;

View File

@ -15,56 +15,62 @@
<button>Login</button> <button>Login</button>
</form> </form>
<pre><code class="js-schedules-output"> </code></pre>
<div class="js-account" hidden> <div class="js-account" hidden>
<h2>Schedules</h2> <details>
<div class="js-schedules"> <summary>Schedules</summary>
<div class="js-schedule"> <h3>Schedules</h3>
<input type="hidden" class="js-id" /> <div class="js-schedules">
<input type="date" class="js-date" readonly /> <div class="js-schedule">
<input type="time" class="js-time" readonly /> <input type="hidden" class="js-id" />
<input type="text" class="js-tz" readonly /> <input type="date" class="js-date" readonly />
<input type="time" class="js-time" readonly />
<input type="text" class="js-tz" readonly />
<div class="doc-webhooks-container"> <div class="doc-webhooks-container">
<div class="js-webhooks"> <div class="js-webhooks">
<div class="js-webhook"> <div class="js-webhook">
<h4><span class="js-comment"></span></h4> <h4><span class="js-comment"></span></h4>
<span class="js-id" hidden></span> <span class="js-id" hidden></span>
<span class="js-method"></span> <span class="js-method"></span>
<span class="js-url"></span> <span class="js-url"></span>
<br /> <br />
<div class="js-headers"> <div class="js-headers">
<div class="js-header"> <div class="js-header">
<span class="js-key"></span> <span class="js-key"></span>
<span class="js-value"></span> <span class="js-value"></span>
</div>
</div> </div>
<pre><code class="js-body-template"></code></pre>
</div> </div>
<pre><code class="js-body-template"></code></pre>
</div> </div>
</div> </div>
<button class="js-delete" type="button">Delete Schedule</button>
<br />
<br />
</div> </div>
<button class="js-delete" type="button">Delete Schedule</button>
</div> </div>
</div>
<h2>Add Schedule</h2>
<form class="js-new-schedule">
<label>Date: <input type="date" class="js-date" required/></label>
<label>Time: <input type="time" class="js-time" step="300" required/></label>
<!-- TODO combo box -->
<label
>Location:
<select class="js-tz">
<option value="UTC">UTC</option>
<option disabled>──────────</option>
</select>
</label>
<br /> <br />
</details>
<h3>Webhook</h3> <details>
<div class="js-new-webhook"> <summary>Add Schedule</summary>
<!-- <h3>Add Schedule</h3>
<form class="js-new-schedule">
<label>Date: <input type="date" class="js-date" required/></label>
<label>Time: <input type="time" class="js-time" step="300" required/></label>
<!-- TODO combo box -->
<label
>Location:
<select class="js-tz">
<option value="UTC">UTC</option>
<option disabled>──────────</option>
</select>
</label>
<br />
<h3>Webhook</h3>
<div class="js-new-webhook">
<!--
<select class="js-template"> <select class="js-template">
<option value="webhook" selected>Custom Webhook</option> <option value="webhook" selected>Custom Webhook</option>
<option value="requestbin">RequestBin</option> <option value="requestbin">RequestBin</option>
@ -74,33 +80,46 @@
</select> </select>
<br /> <br />
--> -->
<input class="js-comment" type="text" placeholder="Webhook Name" required /> <input class="js-comment" type="text" placeholder="Webhook Name" required />
<br /> <br />
<select class="js-method"> <select class="js-method">
<option value="POST" selected>POST</option> <option value="POST" selected>POST</option>
<option value="PUT">PUT</option> <option value="PUT">PUT</option>
</select> </select>
<input placeholder="https://example.com/api/v1/updates" class="js-url" type="url" required /> <input placeholder="https://example.com/api/v1/updates" class="js-url" type="url" required />
<div class="js-headers"> <div class="js-headers">
<div class="js-header"> <div class="js-header">
<input placeholder="Header" class="js-key" type="text" /> <input placeholder="Header" class="js-key" type="text" />
<input placeholder="Value" class="js-value" type="text" /> <input placeholder="Value" class="js-value" type="text" />
<button type="button" class="js-rm-header" hidden>[x]</button> <button type="button" class="js-rm-header" hidden>[x]</button>
<button type="button" class="js-new-header">[+]</button> <button type="button" class="js-new-header">[+]</button>
</div>
</div>
<div class="js-body">
<textarea
placeholder="Body template, use '{{ keyname }}' for template values."
class="js-body-template"
></textarea>
<!-- TODO preview template -->
</div> </div>
</div> </div>
<div class="js-body">
<textarea
placeholder="Body template, use '{{ keyname }}' for template values."
class="js-body-template"
></textarea>
<!-- TODO preview template -->
</div>
</div>
<br />
<button class="js-create">Save Schedule</button>
</form>
<br /> <br />
<button class="js-create">Save Schedule</button> <br />
</form> <br />
</details>
<details>
<summary>Debug Info</summary>
<h3>Debug Info</h3>
<pre><code class="js-schedules-output"> </code></pre>
<br />
<br />
<br />
</details>
</div> </div>
<script src="./ajquery.js"></script> <script src="./ajquery.js"></script>