blob: 13cf1760388de21feb17c86aa8d6b1a3635a14ab [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Handlers for HTTP requests to save and load playground examples.
//
// handlerSave() handles a POST request with bundled playground source code.
// The bundle is persisted in a database and a unique ID returned.
// handlerLoad() handles a GET request with an id parameter. It returns the
// bundle saved under the provided ID or slug, if any.
// handlerListDefault() handles a GET request with no parameters. It returns
// a list of descriptions of all default bundles. Default bundles are saved
// using the pgadmin tool, not the HTTP API.
// The current implementation uses a MySQL-like SQL database for persistence.
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"v.io/x/playground/lib/log"
"v.io/x/playground/lib/storage"
)
//////////////////////////////////////////
// HTTP request handlers
// GET request that returns the saved bundle for the given ID or slug.
func handlerLoad(w http.ResponseWriter, r *http.Request) {
if !handleCORS(w, r) {
return
}
// Check method and read GET parameters.
if !checkGetMethod(w, r) {
return
}
bIdOrSlug := r.FormValue("id")
if bIdOrSlug == "" {
storageError(w, http.StatusBadRequest, "Must specify id to load.")
return
}
bLink, bData, err := storage.GetBundleByLinkIdOrSlug(bIdOrSlug)
if err == storage.ErrNotFound {
storageError(w, http.StatusNotFound, "No data found for provided id.")
return
} else if err != nil {
storageInternalError(w, "Error getting bundleLink for id/slug ", bIdOrSlug, ": ", err)
return
}
storageRespond(w, http.StatusOK, fullResponseFromLinkAndData(bLink, bData))
}
// POST request that saves the body as a new bundle and returns the bundle id.
func handlerSave(w http.ResponseWriter, r *http.Request) {
if !handleCORS(w, r) {
return
}
// Check method and read POST body.
// Limit is set to maxSize+1 to allow distinguishing between exactly maxSize
// and larger than maxSize requests.
requestBody := getPostBody(w, r, *maxSize+1)
if requestBody == nil {
return
}
if len(requestBody) > *maxSize {
storageError(w, http.StatusBadRequest, "Program too large.")
return
}
// TODO(ivanpi): Check if bundle is parseable. Format/lint?
bLink, bData, err := storage.StoreBundleLinkAndData(string(requestBody))
if err != nil {
storageInternalError(w, "Error storing bundle: ", err)
return
}
storageRespond(w, http.StatusOK, fullResponseFromLinkAndData(bLink, bData))
}
// GET request that returns a list of default bundle descriptions.
func handlerListDefault(w http.ResponseWriter, r *http.Request) {
if !handleCORS(w, r) {
return
}
// Check method. No GET parameters are currently used by /list.
if !checkGetMethod(w, r) {
return
}
bList, err := storage.GetDefaultBundleList()
if err != nil {
storageInternalError(w, "Error getting default bundle list: ", err)
return
}
bListResp := make([]*BundleDescResponse, 0, len(bList))
for _, bLink := range bList {
bListResp = append(bListResp, descResponseFromLink(bLink))
}
storageRespond(w, http.StatusOK, bListResp)
}
//////////////////////////////////////////
// Response handling
type ErrorResponse struct {
Error string `json:"error"`
}
type BundleDescResponse struct {
// Bundle ID of the saved/loaded bundle.
Link string `json:"link"`
// Slug of the saved/loaded bundle.
// Currently set only for most recent versions of default bundles.
Slug string `json:"slug,omitempty"`
// Creation timestamp of the loaded bundle.
// Since the timestamp is set by the database, /save responses omit it.
CreatedAt *time.Time `json:"createdAt,omitempty"`
}
type BundleFullResponse struct {
// Bundle description, as sent in /list response.
BundleDescResponse
// Contents of the saved/loaded bundle.
Data string `json:"data"`
}
func descResponseFromLink(bLink *storage.BundleLink) *BundleDescResponse {
return &BundleDescResponse{
Link: bLink.Id,
Slug: string(bLink.Slug),
CreatedAt: zeroTimeToNil(bLink.CreatedAt),
}
}
func fullResponseFromLinkAndData(bLink *storage.BundleLink, bData *storage.BundleData) *BundleFullResponse {
return &BundleFullResponse{
BundleDescResponse: *descResponseFromLink(bLink),
Data: bData.Json,
}
}
// Converts time to pointer, mapping zero time to nil to force it to be
// omitted from JSON. See https://github.com/golang/go/issues/5218
func zeroTimeToNil(t time.Time) *time.Time {
if t.IsZero() {
return nil
}
return &t
}
// Sends response to client. Request handler should exit after this call.
func storageRespond(w http.ResponseWriter, status int, body interface{}) {
bodyJson, _ := json.Marshal(body)
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Length", fmt.Sprintf("%d", len(bodyJson)))
w.WriteHeader(status)
w.Write(bodyJson)
}
// Sends error response with specified message to client.
func storageError(w http.ResponseWriter, status int, msg string) {
storageRespond(w, status, &ErrorResponse{
Error: msg,
})
}
// Logs error internally and sends non-specific error response to client.
func storageInternalError(w http.ResponseWriter, v ...interface{}) {
if len(v) > 0 {
log.Error(v...)
}
storageError(w, http.StatusInternalServerError, "Internal error, please retry.")
}