blob: 829c55c0299108a9a97fc3a800dd17fe336a854e [file] [log] [blame]
package main
import (
"encoding/json"
"flag"
"fmt"
"html/template"
"mime"
"net/http"
"path/filepath"
"runtime"
"github.com/gorilla/mux"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"go.skia.org/infra/scrap/go/client"
"go.skia.org/infra/scrap/go/scrap"
)
// flags
var (
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
resourcesDir = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.")
scrapExchange = flag.String("scrapexchange", "http://scrapexchange:9000", "Scrap exchange service HTTP address.")
)
// server is the state of the server.
type server struct {
scrapClient scrap.ScrapExchange
templates *template.Template
}
func new() (*server, error) {
if *resourcesDir == "" {
_, filename, _, _ := runtime.Caller(0)
*resourcesDir = filepath.Join(filepath.Dir(filename), "../../dist")
}
// Need to set the mime-type for wasm files so streaming compile works.
if err := mime.AddExtensionType(".wasm", "application/wasm"); err != nil {
return nil, err
}
scrapClient, err := client.New(*scrapExchange)
if err != nil {
sklog.Fatalf("Failed to create scrap exchange client: %s", err)
}
srv := &server{
scrapClient: scrapClient,
}
srv.loadTemplates()
return srv, nil
}
func (srv *server) loadTemplates() {
srv.templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles(
filepath.Join(*resourcesDir, "main.html"),
))
}
func (srv *server) templateHandler(filename string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
// Set the HTML to expire at the same time as the JS and WASM, otherwise the HTML
// (and by extension, the JS with its cachbuster hash) might outlive the WASM
// and then the two will skew
w.Header().Set("Cache-Control", "max-age=60")
if *local {
srv.loadTemplates()
}
if err := srv.templates.ExecuteTemplate(w, filename, nil); err != nil {
sklog.Errorf("Failed to expand template %s: %s", filename, err)
}
}
}
func resourceHandler(resourcesDir string) func(http.ResponseWriter, *http.Request) {
fileServer := http.FileServer(http.Dir(resourcesDir))
return func(w http.ResponseWriter, r *http.Request) {
// Use a shorter cache live to limit the risk of canvaskit.js (in indexbundle.js)
// from drifting away from the version of canvaskit.wasm. Ideally, the WASM
// will roll at ToT (~35 commits per day), so living for a minute should
// reduce the risk of JS/WASM being out of sync.
w.Header().Add("Cache-Control", "max-age=60")
fileServer.ServeHTTP(w, r)
}
}
func (srv *server) jsonHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
hashOrName := mux.Vars(r)["hashOrName"]
body, err := srv.scrapClient.LoadScrap(r.Context(), scrap.Particle, hashOrName)
if err != nil {
httputils.ReportError(w, err, "Failed to read JSON file.", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(body); err != nil {
sklog.Errorf("Failed to write response: %s", err)
}
}
func (srv *server) uploadHandler(w http.ResponseWriter, r *http.Request) {
defer util.Close(r.Body)
// Decode Request.
var req scrap.ScrapBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httputils.ReportError(w, err, "Error decoding JSON.", http.StatusBadRequest)
return
}
if req.Type != scrap.Particle {
httputils.ReportError(w, fmt.Errorf("Received invalid scrap type: %q", req.Type), "Invalid Type.", http.StatusBadRequest)
return
}
scrapID, err := srv.scrapClient.CreateScrap(r.Context(), req)
if err != nil {
httputils.ReportError(w, err, "Error creating scrap.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(scrapID); err != nil {
sklog.Errorf("Failed to write response: %s", err)
}
}
func main() {
common.InitWithMust(
"particles",
common.PrometheusOpt(promPort),
common.MetricsLoggingOpt(),
)
srv, err := new()
if err != nil {
sklog.Fatalf("Failed to start: %s", err)
}
r := mux.NewRouter()
r.PathPrefix("/dist/").Handler(http.StripPrefix("/dist/", http.HandlerFunc(httputils.CorsHandler(resourceHandler(*resourcesDir))))).Methods("GET")
r.HandleFunc("/_/j/{hashOrName:[@0-9a-zA-Z-_]+}", srv.jsonHandler).Methods("GET")
r.HandleFunc("/_/upload", srv.uploadHandler).Methods("POST")
r.HandleFunc("/", srv.templateHandler("main.html")).Methods("GET")
// TODO(jcgregorio) Implement CSRF.
h := httputils.LoggingGzipRequestResponse(r)
if !*local {
h = httputils.HealthzAndHTTPS(h)
}
http.Handle("/", h)
sklog.Info("Ready to serve.")
sklog.Fatal(http.ListenAndServe(*port, nil))
}