WIP delete works too now
This commit is contained in:
parent
9db30c7e80
commit
d87b197cc0
14
again.go
14
again.go
|
@ -208,21 +208,21 @@ func IsAmbiguous(st []int, tzstr string) error {
|
|||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
m := time.Month(st[1])
|
||||
t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], st[6], tz)
|
||||
u1 := t1.UTC()
|
||||
// A better way to do this would probably be to parse the timezone database, but... yeah...
|
||||
for _, n := range []int{ /*-120, -60,*/ 30, 60, 120} {
|
||||
// Australia/Lord_Howe has a 30-minute DST
|
||||
// 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)
|
||||
u2 := t2.UTC()
|
||||
if u1.Equal(u2) {
|
||||
fmt.Println("Ambiguous Time")
|
||||
fmt.Printf("%s, %s, %+d\n", t1, u1, n)
|
||||
fmt.Printf("%s, %s, %+d\n", t2, u2, n)
|
||||
return fmt.Errorf("Ambiguous")
|
||||
return fmt.Errorf("Ambiguous: %s, %s, %+d\n", t1, t2, n)
|
||||
}
|
||||
}
|
||||
//ta :=
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ func main() {
|
|||
}
|
||||
//mux.Handle("/api/", http.HandlerFunc(handleFunc))
|
||||
mux.HandleFunc("/api/v0/schedules", s.Handle)
|
||||
mux.HandleFunc("/api/v0/schedules/", s.Handle)
|
||||
|
||||
// TODO Filebox FS
|
||||
mux.Handle("/", http.FileServer(http.Dir("./public")))
|
||||
|
@ -88,6 +89,7 @@ func main() {
|
|||
type ScheduleDB interface {
|
||||
List(string) ([]*again.Schedule, error)
|
||||
Set(again.Schedule) (*again.Schedule, error)
|
||||
Delete(accessID string, id string) (*again.Schedule, error)
|
||||
}
|
||||
|
||||
type scheduler struct {
|
||||
|
@ -107,7 +109,6 @@ func (s *scheduler) Handle(w http.ResponseWriter, r *http.Request) {
|
|||
ctx = context.WithValue(ctx, "token", token)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
fmt.Println("whatever", r.Method, r.URL)
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.List(w, r)
|
||||
|
@ -115,6 +116,9 @@ func (s *scheduler) Handle(w http.ResponseWriter, r *http.Request) {
|
|||
case http.MethodPost:
|
||||
s.Create(w, r)
|
||||
return
|
||||
case http.MethodDelete:
|
||||
s.Delete(w, r)
|
||||
return
|
||||
default:
|
||||
http.Error(w, "Not Implemented", http.StatusNotImplemented)
|
||||
return
|
||||
|
@ -144,10 +148,8 @@ func (s *scheduler) Create(w http.ResponseWriter, r *http.Request) {
|
|||
br, bw := io.Pipe()
|
||||
b := io.TeeReader(r.Body, bw)
|
||||
go func() {
|
||||
fmt.Println("reading from reader...")
|
||||
x, _ := ioutil.ReadAll(b)
|
||||
fmt.Println("cool beans and all")
|
||||
fmt.Println(string(x))
|
||||
fmt.Println("[debug] http body", string(x))
|
||||
bw.Close()
|
||||
}()
|
||||
decoder := json.NewDecoder(br)
|
||||
|
@ -176,3 +178,29 @@ func (s *scheduler) Create(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -50,8 +50,8 @@
|
|||
newWebhookHeader(ev.target);
|
||||
} else if (ev.target.matches('.js-rm-header')) {
|
||||
rmWebhookHeader(ev.target);
|
||||
} else if (ev.target.matches('.js-delete') && ev.target.closest('.js-webhook')) {
|
||||
deleteWebhook(ev.target.closest('.js-webhook'));
|
||||
} else if (ev.target.matches('.js-delete') && ev.target.closest('.js-schedule')) {
|
||||
deleteSchedule(ev.target.closest('.js-schedule'));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
@ -142,11 +142,12 @@
|
|||
throw new Error('something bad happened');
|
||||
}
|
||||
|
||||
state.account.schedules.push(resp.data);
|
||||
state.account.schedules.push(data);
|
||||
|
||||
displayAccount(state.account);
|
||||
})
|
||||
.catch(function(e) {
|
||||
console.error(e);
|
||||
window.alert(e.message);
|
||||
});
|
||||
});
|
||||
|
@ -168,9 +169,8 @@
|
|||
var $h = $rmHeader.closest('.js-header');
|
||||
$h.parentElement.removeChild($h);
|
||||
}
|
||||
function deleteWebhook($hook) {
|
||||
var deviceId = $hook.closest('.js-schedule').querySelector('.js-id').value;
|
||||
var id = $('.js-id', $hook).innerText;
|
||||
function deleteSchedule($sched) {
|
||||
var schedId = $('.js-id', $sched).value;
|
||||
var opts = {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
|
@ -179,27 +179,17 @@
|
|||
},
|
||||
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) {
|
||||
if (!result.webhook) {
|
||||
if (!result.webhooks) {
|
||||
console.error(result);
|
||||
window.alert('something went wrong: ' + JSON.stringify(result));
|
||||
return;
|
||||
}
|
||||
var index = -1;
|
||||
var dev = state.account.devices.filter(function(d, i) {
|
||||
return d.accessToken == deviceId;
|
||||
})[0];
|
||||
dev.webhooks.some(function(g, i) {
|
||||
if (g.id === id) {
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
state.account.schedules = state.account.schedules.filter(function(g) {
|
||||
return g.id !== result.id;
|
||||
});
|
||||
if (index > -1) {
|
||||
dev.webhooks.splice(index, 1);
|
||||
displayAccount(state.account);
|
||||
}
|
||||
displayAccount(state.account);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -211,6 +201,7 @@
|
|||
var $devs = $('.js-schedules');
|
||||
$devs.innerHTML = '';
|
||||
data.schedules.forEach(function(d) {
|
||||
console.log('schedule', d);
|
||||
var $dev = $.create($devTpl);
|
||||
$('.js-id', $dev).value = d.id;
|
||||
$('.js-date', $dev).value = d.date;
|
||||
|
|
|
@ -15,56 +15,62 @@
|
|||
<button>Login</button>
|
||||
</form>
|
||||
|
||||
<pre><code class="js-schedules-output"> </code></pre>
|
||||
|
||||
<div class="js-account" hidden>
|
||||
<h2>Schedules</h2>
|
||||
<div class="js-schedules">
|
||||
<div class="js-schedule">
|
||||
<input type="hidden" class="js-id" />
|
||||
<input type="date" class="js-date" readonly />
|
||||
<input type="time" class="js-time" readonly />
|
||||
<input type="text" class="js-tz" readonly />
|
||||
<details>
|
||||
<summary>Schedules</summary>
|
||||
<h3>Schedules</h3>
|
||||
<div class="js-schedules">
|
||||
<div class="js-schedule">
|
||||
<input type="hidden" class="js-id" />
|
||||
<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="js-webhooks">
|
||||
<div class="js-webhook">
|
||||
<h4><span class="js-comment"></span></h4>
|
||||
<span class="js-id" hidden></span>
|
||||
<span class="js-method"></span>
|
||||
<span class="js-url"></span>
|
||||
<br />
|
||||
<div class="js-headers">
|
||||
<div class="js-header">
|
||||
<span class="js-key"></span>
|
||||
<span class="js-value"></span>
|
||||
<div class="doc-webhooks-container">
|
||||
<div class="js-webhooks">
|
||||
<div class="js-webhook">
|
||||
<h4><span class="js-comment"></span></h4>
|
||||
<span class="js-id" hidden></span>
|
||||
<span class="js-method"></span>
|
||||
<span class="js-url"></span>
|
||||
<br />
|
||||
<div class="js-headers">
|
||||
<div class="js-header">
|
||||
<span class="js-key"></span>
|
||||
<span class="js-value"></span>
|
||||
</div>
|
||||
</div>
|
||||
<pre><code class="js-body-template"></code></pre>
|
||||
</div>
|
||||
<pre><code class="js-body-template"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<button class="js-delete" type="button">Delete Schedule</button>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
<button class="js-delete" type="button">Delete Schedule</button>
|
||||
</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 />
|
||||
</details>
|
||||
|
||||
<h3>Webhook</h3>
|
||||
<div class="js-new-webhook">
|
||||
<!--
|
||||
<details>
|
||||
<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">
|
||||
<option value="webhook" selected>Custom Webhook</option>
|
||||
<option value="requestbin">RequestBin</option>
|
||||
|
@ -74,33 +80,46 @@
|
|||
</select>
|
||||
<br />
|
||||
-->
|
||||
<input class="js-comment" type="text" placeholder="Webhook Name" required />
|
||||
<br />
|
||||
<select class="js-method">
|
||||
<option value="POST" selected>POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
</select>
|
||||
<input placeholder="https://example.com/api/v1/updates" class="js-url" type="url" required />
|
||||
<div class="js-headers">
|
||||
<div class="js-header">
|
||||
<input placeholder="Header" class="js-key" 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-new-header">[+]</button>
|
||||
<input class="js-comment" type="text" placeholder="Webhook Name" required />
|
||||
<br />
|
||||
<select class="js-method">
|
||||
<option value="POST" selected>POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
</select>
|
||||
<input placeholder="https://example.com/api/v1/updates" class="js-url" type="url" required />
|
||||
<div class="js-headers">
|
||||
<div class="js-header">
|
||||
<input placeholder="Header" class="js-key" 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-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 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 />
|
||||
<button class="js-create">Save Schedule</button>
|
||||
</form>
|
||||
<br />
|
||||
<br />
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Debug Info</summary>
|
||||
<h3>Debug Info</h3>
|
||||
<pre><code class="js-schedules-output"> </code></pre>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<script src="./ajquery.js"></script>
|
||||
|
|
Loading…
Reference in New Issue