Finish first API version

This commit is contained in:
Daniel 2018-09-27 15:58:31 +02:00
parent d7e0602548
commit 31c09512a0
6 changed files with 175 additions and 22 deletions

View file

@ -2,11 +2,13 @@ package api
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/tevino/abool" "github.com/tevino/abool"
"github.com/tidwall/gjson"
"github.com/Safing/portbase/container" "github.com/Safing/portbase/container"
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
@ -50,7 +52,7 @@ func startDatabaseAPI(w http.ResponseWriter, r *http.Request) {
} }
wsConn, err := upgrader.Upgrade(w, r, nil) wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
errMsg := fmt.Sprintf("could not upgrade to websocket: %s", err) errMsg := fmt.Sprintf("could not upgrade: %s", err)
log.Error(errMsg) log.Error(errMsg)
http.Error(w, errMsg, 400) http.Error(w, errMsg, 400)
return return
@ -78,11 +80,12 @@ func (api *DatabaseAPI) handler() {
// 124|ok|<key>|<data> // 124|ok|<key>|<data>
// 124|done // 124|done
// 124|error|<message> // 124|error|<message>
// 124|warning|<message> // error with single record, operation continues
// 125|sub|<query> // 125|sub|<query>
// 125|upd|<key>|<data> // 125|upd|<key>|<data>
// 125|new|<key>|<data> // 125|new|<key>|<data>
// 125|delete|<key>|<data> // 125|delete|<key>|<data>
// 125|warning|<message> // does not cancel the subscription // 125|warning|<message> // error with single record, operation continues
// 127|qsub|<query> // 127|qsub|<query>
// 127|ok|<key>|<data> // 127|ok|<key>|<data>
// 127|done // 127|done
@ -90,7 +93,7 @@ func (api *DatabaseAPI) handler() {
// 127|upd|<key>|<data> // 127|upd|<key>|<data>
// 127|new|<key>|<data> // 127|new|<key>|<data>
// 127|delete|<key>|<data> // 127|delete|<key>|<data>
// 127|warning|<message> // does not cancel the subscription // 127|warning|<message> // error with single record, operation continues
// 128|create|<key>|<data> // 128|create|<key>|<data>
// 128|success // 128|success
@ -115,7 +118,7 @@ func (api *DatabaseAPI) handler() {
return return
} }
parts := bytes.SplitN(msg, []byte("|"), 2) parts := bytes.SplitN(msg, []byte("|"), 3)
if len(parts) != 3 { if len(parts) != 3 {
api.send(nil, dbMsgTypeError, []byte("bad request: malformed message")) api.send(nil, dbMsgTypeError, []byte("bad request: malformed message"))
continue continue
@ -137,7 +140,7 @@ func (api *DatabaseAPI) handler() {
case "create", "update", "insert": case "create", "update", "insert":
// split key and payload // split key and payload
dataParts := bytes.SplitN(parts[2], []byte("|"), 1) dataParts := bytes.SplitN(parts[2], []byte("|"), 2)
if len(dataParts) != 2 { if len(dataParts) != 2 {
api.send(nil, dbMsgTypeError, []byte("bad request: malformed message")) api.send(nil, dbMsgTypeError, []byte("bad request: malformed message"))
continue continue
@ -146,10 +149,10 @@ func (api *DatabaseAPI) handler() {
switch string(parts[1]) { switch string(parts[1]) {
case "create": case "create":
// 128|create|<key>|<data> // 128|create|<key>|<data>
go api.handleCreate(parts[0], string(dataParts[0]), dataParts[1]) go api.handlePut(parts[0], string(dataParts[0]), dataParts[1], true)
case "update": case "update":
// 129|update|<key>|<data> // 129|update|<key>|<data>
go api.handleUpdate(parts[0], string(dataParts[0]), dataParts[1]) go api.handlePut(parts[0], string(dataParts[0]), dataParts[1], false)
case "insert": case "insert":
// 130|insert|<key>|<data> // 130|insert|<key>|<data>
go api.handleInsert(parts[0], string(dataParts[0]), dataParts[1]) go api.handleInsert(parts[0], string(dataParts[0]), dataParts[1])
@ -224,6 +227,7 @@ func (api *DatabaseAPI) handleQuery(opID []byte, queryText string) {
// 124|done // 124|done
// 124|warning|<message> // 124|warning|<message>
// 124|error|<message> // 124|error|<message>
// 124|warning|<message> // error with single record, operation continues
var err error var err error
@ -250,8 +254,8 @@ func (api *DatabaseAPI) processQuery(opID []byte, q *query.Query) (ok bool) {
} }
api.send(opID, dbMsgTypeOk, data) api.send(opID, dbMsgTypeOk, data)
} }
if it.Error != nil { if it.Err != nil {
api.send(opID, dbMsgTypeError, []byte(err.Error())) api.send(opID, dbMsgTypeError, []byte(it.Err.Error()))
return false return false
} }
@ -266,7 +270,7 @@ func (api *DatabaseAPI) handleSub(opID []byte, queryText string) {
// 125|upd|<key>|<data> // 125|upd|<key>|<data>
// 125|new|<key>|<data> // 125|new|<key>|<data>
// 125|delete|<key> // 125|delete|<key>
// 125|warning|<message> // does not cancel the subscription // 125|warning|<message> // error with single record, operation continues
var err error var err error
q, err := query.ParseQuery(queryText) q, err := query.ParseQuery(queryText)
@ -314,7 +318,7 @@ func (api *DatabaseAPI) handleQsub(opID []byte, queryText string) {
// 127|upd|<key>|<data> // 127|upd|<key>|<data>
// 127|new|<key>|<data> // 127|new|<key>|<data>
// 127|delete|<key> // 127|delete|<key>
// 127|warning|<message> // does not cancel the subscription // 127|warning|<message> // error with single record, operation continues
var err error var err error
@ -335,20 +339,92 @@ func (api *DatabaseAPI) handleQsub(opID []byte, queryText string) {
api.processSub(opID, sub) api.processSub(opID, sub)
} }
func (api *DatabaseAPI) handleCreate(opID []byte, key string, data []byte) { func (api *DatabaseAPI) handlePut(opID []byte, key string, data []byte, create bool) {
// 128|create|<key>|<data> // 128|create|<key>|<data>
// 128|success // 128|success
// 128|error|<message> // 128|error|<message>
}
func (api *DatabaseAPI) handleUpdate(opID []byte, key string, data []byte) {
// 129|update|<key>|<data> // 129|update|<key>|<data>
// 129|success // 129|success
// 129|error|<message> // 129|error|<message>
raw := make([]byte, len(data)+1)
raw[0] = record.JSON
copy(raw[1:], data)
dbName, dbKey := record.ParseKey(key)
r, err := record.NewRawWrapper(dbName, dbKey, raw)
if err != nil {
api.send(opID, dbMsgTypeError, []byte(err.Error()))
return
}
if create {
err = api.db.PutNew(r)
} else {
err = api.db.Put(r)
}
if err != nil {
api.send(opID, dbMsgTypeError, []byte(err.Error()))
return
}
api.send(opID, dbMsgTypeSuccess, nil)
} }
func (api *DatabaseAPI) handleInsert(opID []byte, key string, data []byte) { func (api *DatabaseAPI) handleInsert(opID []byte, key string, data []byte) {
// 130|insert|<key>|<data> // 130|insert|<key>|<data>
// 130|success // 130|success
// 130|error|<message> // 130|error|<message>
r, err := api.db.Get(key)
if err != nil {
api.send(opID, dbMsgTypeError, []byte(err.Error()))
return
}
acc := r.GetAccessor(r)
result := gjson.ParseBytes(data)
anythingPresent := false
var insertError error
result.ForEach(func(key gjson.Result, value gjson.Result) bool {
anythingPresent = true
if !key.Exists() {
insertError = errors.New("values must be in a map")
return false
}
if key.Type != gjson.String {
insertError = errors.New("keys must be strings")
return false
}
if !value.Exists() {
insertError = errors.New("non-existent value")
return false
}
insertError = acc.Set(key.String(), value.Value())
if insertError != nil {
return false
}
return true
})
if insertError != nil {
api.send(opID, dbMsgTypeError, []byte(insertError.Error()))
return
}
if !anythingPresent {
api.send(opID, dbMsgTypeError, []byte("could not find any valid values"))
return
}
err = api.db.Put(r)
if err != nil {
api.send(opID, dbMsgTypeError, []byte(err.Error()))
return
}
api.send(opID, dbMsgTypeSuccess, nil)
} }
func (api *DatabaseAPI) shutdown() { func (api *DatabaseAPI) shutdown() {

22
api/enriched-response.go Normal file
View file

@ -0,0 +1,22 @@
package api
import (
"net/http"
)
type EnrichedResponseWriter struct {
http.ResponseWriter
Status int
}
func NewEnrichedResponseWriter(w http.ResponseWriter) *EnrichedResponseWriter {
return &EnrichedResponseWriter{
w,
0,
}
}
func (ew *EnrichedResponseWriter) WriteHeader(code int) {
ew.Status = code
ew.ResponseWriter.WriteHeader(code)
}

22
api/main.go Normal file
View file

@ -0,0 +1,22 @@
package api
import (
"github.com/Safing/portbase/modules"
)
func init() {
modules.Register("api", prep, start, stop, "database")
}
func prep() error {
return nil
}
func start() error {
go Serve()
return nil
}
func stop() error {
return nil
}

View file

@ -4,26 +4,44 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/Safing/portbase/log"
) )
var ( var (
additionalRoutes map[string]func(arg1 http.ResponseWriter, arg2 *http.Request) additionalRoutes map[string]http.Handler
) )
func RegisterAdditionalRoute(path string, handleFunc func(arg1 http.ResponseWriter, arg2 *http.Request)) { func RegisterAdditionalRoute(path string, handler http.Handler) {
if additionalRoutes == nil { if additionalRoutes == nil {
additionalRoutes = make(map[string]func(arg1 http.ResponseWriter, arg2 *http.Request)) additionalRoutes = make(map[string]http.Handler)
} }
additionalRoutes[path] = handleFunc additionalRoutes[path] = handler
}
func logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ew := NewEnrichedResponseWriter(w)
next.ServeHTTP(ew, r)
log.Infof("api request: %s %d %s", r.RemoteAddr, ew.Status, r.RequestURI)
})
} }
func Serve() { func Serve() {
router := mux.NewRouter() router := mux.NewRouter()
router.HandleFunc("/api/database/v1", startDatabaseAPI) // router.HandleFunc("/api/database/v1", startDatabaseAPI)
for path, handleFunc := range additionalRoutes { for path, handler := range additionalRoutes {
router.HandleFunc(path, handleFunc) router.Handle(path, handler)
} }
router.Use(logger)
http.Handle("/", router)
http.HandleFunc("/api/database/v1", startDatabaseAPI)
address := "127.0.0.1:18"
log.Infof("api: starting to listen on %s", address)
log.Errorf("api: failed to listen on %s: %s", address, http.ListenAndServe(address, nil))
} }

View file

@ -7,7 +7,7 @@
<body> <body>
<script type="text/javascript"> <script type="text/javascript">
var ws = new WebSocket('ws://localhost:18/api/v1'); var ws = new WebSocket('ws://127.0.0.1:18/api/database/v1')
ws.onopen = function () { ws.onopen = function () {
console.log('open'); console.log('open');
@ -26,6 +26,10 @@
reader.readAsText(e.data) reader.readAsText(e.data)
}; };
function send(text) {
ws.send(text)
}
// var sock = new SockJS("http://localhost:8080/api/v1"); // var sock = new SockJS("http://localhost:8080/api/v1");
// //
// sock.onopen = function() { // sock.onopen = function() {

11
api/testclient/serve.go Normal file
View file

@ -0,0 +1,11 @@
package testclient
import (
"net/http"
"github.com/Safing/portbase/api"
)
func init() {
api.RegisterAdditionalRoute("/test/", http.StripPrefix("/test/", http.FileServer(http.Dir("./api/testclient/root/"))))
}