2021-05-08 16:03:21 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
2021-05-16 09:12:38 +00:00
|
|
|
"log"
|
2021-05-08 16:03:21 +00:00
|
|
|
"net/http"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"gitlab.com/mporrato/uBrowserSync/syncstore"
|
|
|
|
)
|
|
|
|
|
2021-05-17 15:31:34 +00:00
|
|
|
const (
|
|
|
|
apiVersion = "1.1.13"
|
|
|
|
infoMessage = "Powered by uBrowserSync"
|
|
|
|
defaultMaxSyncSize = 512000
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
store syncstore.Store
|
|
|
|
maxSyncSize = defaultMaxSyncSize
|
|
|
|
listen string
|
|
|
|
serviceStatus = 1
|
|
|
|
sidRe = regexp.MustCompile("^[[:xdigit:]]{32}$")
|
|
|
|
|
|
|
|
invalidRequestError = syncstore.NewSyncError(
|
|
|
|
"Invalid request",
|
|
|
|
"Malformed request body",
|
|
|
|
http.StatusBadRequest)
|
|
|
|
)
|
2021-05-16 15:46:05 +00:00
|
|
|
|
2021-05-08 16:03:21 +00:00
|
|
|
type serviceInfoResp struct {
|
|
|
|
Version string `json:"version"`
|
|
|
|
Message string `json:"message"`
|
2021-05-16 19:11:45 +00:00
|
|
|
MaxSyncSize int `json:"maxSyncSize"`
|
2021-05-08 16:03:21 +00:00
|
|
|
Status int `json:"status"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type CreateReq struct {
|
|
|
|
Version string `json:"version"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type UpdateReq struct {
|
|
|
|
Bookmarks string `json:"bookmarks"`
|
|
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type UpdateResp struct {
|
|
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type LastUpdatedResp struct {
|
|
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type GetSyncVerResp struct {
|
|
|
|
Version string `json:"version"`
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
func sendJSON(w http.ResponseWriter, status int, data interface{}) {
|
2021-05-08 16:03:21 +00:00
|
|
|
body, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
2021-05-16 12:51:39 +00:00
|
|
|
log.Printf("sendJSON(%v): json.Marshal() failed: %v", data, err)
|
|
|
|
return
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(status)
|
|
|
|
_, err = w.Write(body)
|
2021-05-16 12:51:39 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println("sendJSON() failed:", err)
|
|
|
|
}
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
func sendJSONOk(w http.ResponseWriter, data interface{}) {
|
|
|
|
sendJSON(w, http.StatusOK, data)
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
func sendJSONError(w http.ResponseWriter, err error) {
|
2021-05-08 16:03:21 +00:00
|
|
|
switch e := err.(type) {
|
|
|
|
case syncstore.SyncError:
|
2021-05-16 12:51:39 +00:00
|
|
|
sendJSON(w, e.StatusCode, e.Payload)
|
2021-05-08 16:03:21 +00:00
|
|
|
default:
|
2021-05-16 12:51:39 +00:00
|
|
|
sendJSON(w, http.StatusInternalServerError,
|
2021-05-08 16:03:21 +00:00
|
|
|
syncstore.ErrorPayload{
|
|
|
|
Code: "Internal server error",
|
|
|
|
Message: e.Error()})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func info(w http.ResponseWriter, req *http.Request) {
|
|
|
|
if req.Method != "GET" || req.URL.Path != "/info" {
|
|
|
|
sendJSONError(w, syncstore.NotImplementedError)
|
|
|
|
} else {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Println("info()")
|
2021-05-16 15:46:05 +00:00
|
|
|
serviceInfo := serviceInfoResp{
|
2021-05-16 19:11:45 +00:00
|
|
|
Version: apiVersion,
|
|
|
|
Message: infoMessage,
|
|
|
|
Status: serviceStatus,
|
2021-05-16 15:46:05 +00:00
|
|
|
MaxSyncSize: maxSyncSize}
|
|
|
|
sendJSONOk(w, serviceInfo)
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func createSync(w http.ResponseWriter, req *http.Request) {
|
|
|
|
body := new(CreateReq)
|
|
|
|
err := json.NewDecoder(req.Body).Decode(&body)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, invalidRequestError)
|
|
|
|
return
|
|
|
|
}
|
2021-05-16 15:46:05 +00:00
|
|
|
if len(body.Version) > 20 {
|
|
|
|
sendJSONError(w, invalidRequestError)
|
|
|
|
return
|
|
|
|
}
|
2021-05-08 16:03:21 +00:00
|
|
|
resp, err := store.Create(body.Version)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONOk(w, resp)
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
func getSync(syncId string, w http.ResponseWriter, _ *http.Request) {
|
2021-05-08 16:03:21 +00:00
|
|
|
resp, err := store.Get(syncId)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONOk(w, resp)
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
func getLastUpdated(syncId string, w http.ResponseWriter, _ *http.Request) {
|
2021-05-08 16:03:21 +00:00
|
|
|
resp, err := store.GetLastUpdated(syncId)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONOk(w, LastUpdatedResp{LastUpdated: resp})
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
func getVersion(syncId string, w http.ResponseWriter, _ *http.Request) {
|
2021-05-08 16:03:21 +00:00
|
|
|
resp, err := store.GetVersion(syncId)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONOk(w, GetSyncVerResp{Version: resp})
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateSync(syncId string, w http.ResponseWriter, req *http.Request) {
|
|
|
|
body := new(UpdateReq)
|
|
|
|
err := json.NewDecoder(req.Body).Decode(&body)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, invalidRequestError)
|
|
|
|
return
|
|
|
|
}
|
2021-05-16 15:46:05 +00:00
|
|
|
if len(body.Bookmarks) > maxSyncSize {
|
|
|
|
sendJSONError(w, syncstore.SyncDataLimitExceededError)
|
|
|
|
return
|
|
|
|
}
|
2021-05-08 16:03:21 +00:00
|
|
|
resp, err := store.Update(syncId, body.Bookmarks, body.LastUpdated)
|
|
|
|
if err != nil {
|
|
|
|
sendJSONError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONOk(w, UpdateResp{LastUpdated: resp})
|
|
|
|
}
|
|
|
|
|
|
|
|
func bookmarks(w http.ResponseWriter, req *http.Request) {
|
|
|
|
elements := strings.Split(strings.Trim(req.URL.Path, "/"), "/")
|
|
|
|
|
|
|
|
if len(elements) == 1 && req.Method == "POST" {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Println("createSync()")
|
2021-05-08 16:03:21 +00:00
|
|
|
createSync(w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
syncId := elements[1]
|
|
|
|
if !sidRe.MatchString(elements[1]) {
|
|
|
|
sendJSONError(w, syncstore.NotImplementedError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(elements) == 2 {
|
|
|
|
if req.Method == "GET" {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Printf("getSync(%s)", syncId)
|
2021-05-08 16:03:21 +00:00
|
|
|
getSync(syncId, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if req.Method == "PUT" {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Printf("updateSync(%s)", syncId)
|
2021-05-08 16:03:21 +00:00
|
|
|
updateSync(syncId, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// TODO: Handle HEAD requests
|
|
|
|
sendJSONError(w, syncstore.MethodNotImplementedError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(elements) == 3 {
|
|
|
|
if elements[2] == "lastUpdated" {
|
|
|
|
if req.Method == "GET" {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Printf("getLastUpdated(%s)", syncId)
|
2021-05-08 16:03:21 +00:00
|
|
|
getLastUpdated(syncId, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONError(w, syncstore.MethodNotImplementedError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if elements[2] == "version" {
|
|
|
|
if req.Method == "GET" {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Printf("getVersion(%s)", syncId)
|
2021-05-08 16:03:21 +00:00
|
|
|
getVersion(syncId, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendJSONError(w, syncstore.MethodNotImplementedError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sendJSONError(w, syncstore.NotImplementedError)
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2021-05-17 15:31:34 +00:00
|
|
|
var (
|
|
|
|
err error
|
|
|
|
storeFlag string
|
|
|
|
storeDrv syncstore.StoreDriver
|
|
|
|
)
|
2021-05-08 16:03:21 +00:00
|
|
|
|
|
|
|
flag.StringVar(&listen, "listen", ":8090", "listen address and port")
|
2021-05-16 12:51:39 +00:00
|
|
|
flag.StringVar(&storeFlag, "store", "fs:data", "blob store driver")
|
2021-05-16 15:46:05 +00:00
|
|
|
flag.IntVar(&maxSyncSize, "maxsize", defaultMaxSyncSize, "maximum size of a sync in bytes")
|
2021-05-08 16:03:21 +00:00
|
|
|
flag.Parse()
|
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
storeFlagTokens := strings.Split(storeFlag, ":")
|
2021-05-08 16:03:21 +00:00
|
|
|
|
2021-05-16 12:51:39 +00:00
|
|
|
switch storeFlagTokens[0] {
|
2021-05-08 16:03:21 +00:00
|
|
|
case "fs":
|
2021-05-16 12:51:39 +00:00
|
|
|
if len(storeFlagTokens) != 2 {
|
2021-05-08 16:03:21 +00:00
|
|
|
err = fmt.Errorf("argument required for fs store driver")
|
|
|
|
} else {
|
2021-05-16 12:51:39 +00:00
|
|
|
storeDrv, err = syncstore.NewFSStore(storeFlagTokens[1])
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
case "mem":
|
|
|
|
storeDrv, err = syncstore.NewMemStore()
|
|
|
|
default:
|
2021-05-16 19:11:45 +00:00
|
|
|
err = fmt.Errorf("Invalid store driver: " + storeFlagTokens[0])
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Fatalf("store initialization failed: %v", err)
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|
|
|
|
store = syncstore.NewStore(storeDrv)
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
http.HandleFunc("/info", info)
|
|
|
|
http.HandleFunc("/bookmarks", bookmarks)
|
|
|
|
http.HandleFunc("/bookmarks/", bookmarks)
|
|
|
|
|
2021-05-16 09:12:38 +00:00
|
|
|
log.Println("HTTP server listening on", listen)
|
|
|
|
err := http.ListenAndServe(listen, nil)
|
|
|
|
log.Println("HTTP server terminated", err)
|
2021-05-08 16:03:21 +00:00
|
|
|
}
|