| package main |
| |
| import ( |
| "crypto/md5" |
| "flag" |
| "fmt" |
| "html/template" |
| "net/http" |
| "strings" |
| "sync" |
| "time" |
| |
| "github.com/gorilla/mux" |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/common" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/sklog" |
| sheets "google.golang.org/api/sheets/v4" |
| ) |
| |
| const ( |
| index = `<!DOCTYPE html> |
| <html> |
| <head> |
| <title>Skia Drawing Contest</title> |
| <meta charset="utf-8" /> |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <style> |
| body { |
| color: #000; |
| background: #FFF; |
| font-family: Helvetica,Arial,'Bitstream Vera Sans',sans-serif; |
| margin: 0; |
| padding: 0; |
| } |
| |
| .entry { |
| text-align: center; |
| border: solid lightgray 1px; |
| padding: 1em; |
| margin: 1em; |
| float: left;' |
| } |
| |
| a { |
| color: #1f78b4; |
| } |
| |
| h2 { |
| background: #1f78b4; |
| color: white; |
| padding: 1em; |
| margin: 0; |
| } |
| |
| p { |
| margin: 1em; |
| } |
| </style> |
| </head> |
| <body> |
| <h2>Skia Coding Contest</h2> |
| <p> |
| Create your entry at <a target=_blank href="https://fiddle.skia.org">fiddle.skia.org</a> and submit the URL of your completed fiddle <a href="http://goo.gl/forms/pRfo39hTND">here</a>. |
| </p> |
| {{range .Values}} |
| <div> |
| <div class=entry> |
| <a href='https://fiddle.skia.org/c/{{.Hash}}'> |
| <video autoplay loop width=256 height=256 src='https://fiddle.skia.org/i/{{.Hash}}_cpu.webm' poster='https://fiddle.skia.org/i/{{.Hash}}_raster.png'></video> |
| </a> |
| <div style='padding-top: 1em;'><b>{{.Name}}</b></div> |
| </div> |
| </div> |
| {{end}} |
| <script type="text/javascript" charset="utf-8"> |
| // Hit /update and check for an updated hash value and if it has changed then refresh the page. |
| function checkForUpdates() { |
| var xhr = new XMLHttpRequest(); |
| xhr.open("GET", "/update", true); |
| xhr.onreadystatechange = function(e) { |
| if (e.currentTarget.readyState === XMLHttpRequest.DONE) { |
| if (e.currentTarget.responseText === "{{.Hash}}") { |
| window.setTimeout(checkForUpdates, 5000); |
| } else { |
| window.location.reload(true); |
| } |
| } |
| }; |
| xhr.send(); |
| } |
| |
| checkForUpdates(); |
| </script> |
| </body> |
| </html>` |
| ) |
| |
| // flags |
| var ( |
| local = flag.Bool("local", false, "Running locally, not in prod.") |
| port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')") |
| promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')") |
| ) |
| |
| // Values contains the fiddle hash and the user's name of a single contest entry. |
| type Values struct { |
| Hash string |
| Name string |
| } |
| |
| // Context is used to expand the HTML template. |
| type Context struct { |
| Values []*Values |
| Hash string |
| } |
| |
| var ( |
| indexTemplate = template.Must(template.New("index").Parse(index)) |
| ss *sheets.Service |
| |
| // mutex protects values and valueHash. |
| mutex sync.Mutex |
| values []*Values |
| valuesHash string // valueHash is an md5 hash of values. |
| ) |
| |
| func mainHandler(w http.ResponseWriter, r *http.Request) { |
| mutex.Lock() |
| defer mutex.Unlock() |
| w.Header().Set("Content-Type", "text/html") |
| if err := indexTemplate.Execute(w, Context{ |
| Values: values, |
| Hash: valuesHash, |
| }); err != nil { |
| sklog.Errorf("Failed to expand template: %s", err) |
| } |
| } |
| |
| // updateHandler returns valuesHash. |
| func updateHandler(w http.ResponseWriter, r *http.Request) { |
| mutex.Lock() |
| defer mutex.Unlock() |
| if _, err := w.Write([]byte(valuesHash)); err != nil { |
| sklog.Errorf("Failed to write hash response: %s", err) |
| } |
| } |
| |
| // hash computes an md5 hash of date in 'values'. |
| func hash(values []*Values) string { |
| h := md5.New() |
| for _, v := range values { |
| _, _ = h.Write([]byte(v.Hash)) |
| _, _ = h.Write([]byte(v.Name)) |
| } |
| return fmt.Sprintf("%x", h.Sum(nil)) |
| } |
| |
| // step does a single update of 'values' and 'valueHash' from the Google spreadsheet. |
| func step() error { |
| v, err := ss.Spreadsheets.Values.Get("1Jbv7pWwH8NwHtoEBez1Bkwmk2wjhMnCk9UKttYqPjNQ", "A1:C200").Do() |
| if err != nil { |
| return err |
| } |
| // Should parse out the hash from the URL, also drop the first row since that's the header. |
| newValues := []*Values{} |
| for _, slice := range v.Values { |
| parts := strings.Split(slice[1].(string), "/") |
| if len(parts) != 5 { |
| continue |
| } |
| hash := parts[4] |
| if hash == "" { |
| continue |
| } |
| newValues = append(newValues, &Values{ |
| Hash: hash, |
| Name: slice[2].(string), |
| }) |
| } |
| mutex.Lock() |
| defer mutex.Unlock() |
| values = newValues |
| valuesHash = hash(values) |
| return nil |
| } |
| |
| func main() { |
| common.InitWithMust( |
| "contest", |
| common.PrometheusOpt(promPort), |
| common.MetricsLoggingOpt(), |
| ) |
| |
| ts, err := auth.NewDefaultTokenSource(*local, sheets.SpreadsheetsReadonlyScope) |
| if err != nil { |
| sklog.Fatalf("Failed to auth: %s", err) |
| } |
| client := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client() |
| ss, err = sheets.New(client) |
| if err != nil { |
| sklog.Fatalf("Failed to create Sheets client: %s", err) |
| } |
| |
| if err := step(); err != nil { |
| sklog.Fatalf("Failed initial population of contest entries: %s", err) |
| } |
| go func() { |
| for range time.Tick(15 * time.Second) { |
| if err := step(); err != nil { |
| sklog.Fatalf("Failed to refresh from Google Sheets: %s", err) |
| } |
| } |
| }() |
| r := mux.NewRouter() |
| r.HandleFunc("/", mainHandler) |
| r.HandleFunc("/update", updateHandler) |
| h := httputils.LoggingGzipRequestResponse(r) |
| if !*local { |
| h = httputils.HealthzAndHTTPS(h) |
| } |
| http.Handle("/", h) |
| sklog.Infoln("Ready to serve.") |
| sklog.Fatal(http.ListenAndServe(*port, nil)) |
| } |