Refactor frontend as prep for merging sandbox and frontend
Change-Id: I2c7f5c6f11134aec1fafa5d3963adfbcbc883690
Reviewed-on: https://go-review.googlesource.com/84915
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/README.md b/README.md
index 88e2951..4b4f986 100644
--- a/README.md
+++ b/README.md
@@ -14,15 +14,17 @@
docker build -t frontend frontend/
```
-### Dev Setup
+### Running with an in-memory store
```
+docker run --rm -d -p 8080:8080 frontend
+```
+
+### Running with the Cloud Datastore Emulator
+
+```
+# install it if needed
gcloud components install cloud-datastore-emulator
-```
-
-### Running
-
-```
# run the datastore emulator
gcloud --project=golang-org beta emulators datastore start
# set env vars
@@ -33,6 +35,11 @@
Now visit localhost:8080 to ensure it worked.
+```
+# unset any env vars once finished
+$(gcloud beta emulators datastore env-unset)
+```
+
## Sandbox
### Building
diff --git a/frontend/compile.go b/frontend/compile.go
index 1d1b39a..0c4deb5 100644
--- a/frontend/compile.go
+++ b/frontend/compile.go
@@ -1,39 +1,37 @@
-// Copyright 2011 The Go Authors. All rights reserved.
+// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
- "fmt"
"io"
"net/http"
- "os"
)
const runURL = "https://golang.org/compile?output=json"
-func compile(w http.ResponseWriter, r *http.Request) {
+func (s *server) handleCompile(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
- if err := passThru(w, r); err != nil {
+ if err := s.passThru(w, r); err != nil {
http.Error(w, "Compile server error.", http.StatusInternalServerError)
return
}
}
-func passThru(w io.Writer, req *http.Request) error {
+func (s *server) passThru(w io.Writer, req *http.Request) error {
defer req.Body.Close()
r, err := http.Post(runURL, req.Header.Get("Content-type"), req.Body)
if err != nil {
- fmt.Fprintf(os.Stderr, "error making POST request: %v", err)
+ s.log.Errorf("error making POST request: %v", err)
return err
}
defer r.Body.Close()
if _, err := io.Copy(w, r.Body); err != nil {
- fmt.Fprintf(os.Stderr, "error copying response Body: %v", err)
+ s.log.Errorf("error copying response Body: %v", err)
return err
}
return nil
diff --git a/frontend/edit.go b/frontend/edit.go
index f24fcb7..5edd633 100644
--- a/frontend/edit.go
+++ b/frontend/edit.go
@@ -1,4 +1,4 @@
-// Copyright 2011 The Go Authors. All rights reserved.
+// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
@@ -8,7 +8,6 @@
"fmt"
"html/template"
"net/http"
- "os"
"strings"
"cloud.google.com/go/datastore"
@@ -23,7 +22,7 @@
Share bool
}
-func edit(w http.ResponseWriter, r *http.Request) {
+func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) {
// Redirect foo.play.golang.org to play.golang.org.
if strings.HasSuffix(r.Host, "."+hostname) {
http.Redirect(w, r, "https://"+hostname, http.StatusFound)
@@ -44,11 +43,10 @@
id = id[:len(id)-3]
serveText = true
}
- key := datastore.NameKey("Snippet", id, nil)
- err := datastoreClient.Get(ctx, key, snip)
- if err != nil {
+
+ if err := s.db.GetSnippet(ctx, id, snip); err != nil {
if err != datastore.ErrNoSuchEntity {
- fmt.Fprintf(os.Stderr, "loading Snippet: %v", err)
+ s.log.Errorf("loading Snippet: %v", err)
}
http.Error(w, "Snippet not found", http.StatusNotFound)
return
@@ -59,7 +57,7 @@
"Content-Disposition", fmt.Sprintf(`attachment; filename="%s.go"`, id),
)
}
- w.Header().Set("Content-type", "text/plain")
+ w.Header().Set("Content-type", "text/plain; charset=utf-8")
w.Write(snip.Body)
return
}
diff --git a/frontend/fmt.go b/frontend/fmt.go
index ad916fa..571c68a 100644
--- a/frontend/fmt.go
+++ b/frontend/fmt.go
@@ -1,4 +1,4 @@
-// Copyright 2012 The Go Authors. All rights reserved.
+// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
@@ -17,7 +17,7 @@
Error string
}
-func fmtHandler(w http.ResponseWriter, r *http.Request) {
+func handleFmt(w http.ResponseWriter, r *http.Request) {
var (
in = []byte(r.FormValue("body"))
out []byte
diff --git a/frontend/frontend.go b/frontend/frontend.go
deleted file mode 100644
index 55cb22e..0000000
--- a/frontend/frontend.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2013 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "io"
- "log"
- "net/http"
- "os"
-
- "cloud.google.com/go/compute/metadata"
- "cloud.google.com/go/datastore"
- "golang.org/x/tools/godoc/static"
-)
-
-var datastoreClient *datastore.Client
-
-func main() {
- flag.Parse()
-
- var err error
- datastoreClient, err = datastore.NewClient(context.Background(), projectID())
- if err != nil {
- log.Fatal(err)
- }
-
- http.Handle("/", hstsHandler(edit))
- http.Handle("/compile", hstsHandler(compile))
- http.Handle("/fmt", hstsHandler(fmtHandler))
- http.Handle("/share", hstsHandler(share))
- http.Handle("/playground.js", hstsHandler(play))
- staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))
- http.Handle("/static/", hstsHandler(staticHandler.(http.HandlerFunc)))
- http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
- http.ServeFile(w, r, "./static/favicon.ico")
- })
- http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "ok")
- })
- port := os.Getenv("PORT")
- if port == "" {
- port = "8080"
- }
- log.Printf("Listening on :%v ...", port)
- log.Fatal(http.ListenAndServe(":"+port, nil))
-}
-
-func projectID() string {
- id := os.Getenv("DATASTORE_PROJECT_ID")
- if id != "" {
- return id
- }
- id, err := metadata.ProjectID()
- if err != nil {
- log.Fatalf("Could not determine the project ID (%v); If running locally, ensure DATASTORE_PROJECT_ID is set.", err)
- }
- return id
-}
-
-func play(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-type", "text/javascript")
- io.WriteString(w, static.Files["playground.js"])
-}
diff --git a/frontend/hsts.go b/frontend/hsts.go
deleted file mode 100644
index c463757..0000000
--- a/frontend/hsts.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2016 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package main
-
-import "net/http"
-
-// hstsHandler wraps an http.HandlerFunc such that it sets the HSTS header.
-func hstsHandler(fn http.HandlerFunc) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
- fn(w, r)
- })
-}
diff --git a/frontend/logger.go b/frontend/logger.go
new file mode 100644
index 0000000..2d36289
--- /dev/null
+++ b/frontend/logger.go
@@ -0,0 +1,43 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ stdlog "log"
+ "os"
+)
+
+type logger interface {
+ Printf(format string, args ...interface{})
+ Errorf(format string, args ...interface{})
+ Fatalf(format string, args ...interface{})
+}
+
+// stdLogger implements the logger interface using the log package.
+// There is no need to specify a date/time prefix since stdout and stderr
+// are logged in StackDriver with those values already present.
+type stdLogger struct {
+ stderr *stdlog.Logger
+ stdout *stdlog.Logger
+}
+
+func newStdLogger() *stdLogger {
+ return &stdLogger{
+ stdout: stdlog.New(os.Stdout, "", 0),
+ stderr: stdlog.New(os.Stderr, "", 0),
+ }
+}
+
+func (l *stdLogger) Printf(format string, args ...interface{}) {
+ l.stdout.Printf(format, args...)
+}
+
+func (l *stdLogger) Errorf(format string, args ...interface{}) {
+ l.stderr.Printf(format, args...)
+}
+
+func (l *stdLogger) Fatalf(format string, args ...interface{}) {
+ l.stderr.Fatalf(format, args...)
+}
diff --git a/frontend/main.go b/frontend/main.go
new file mode 100644
index 0000000..497f49c
--- /dev/null
+++ b/frontend/main.go
@@ -0,0 +1,55 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+
+ "cloud.google.com/go/compute/metadata"
+ "cloud.google.com/go/datastore"
+)
+
+var log = newStdLogger()
+
+func main() {
+ s, err := newServer(func(s *server) error {
+ pid := projectID()
+ if pid == "" {
+ s.db = &inMemStore{}
+ } else {
+ c, err := datastore.NewClient(context.Background(), pid)
+ if err != nil {
+ return fmt.Errorf("could not create cloud datastore client: %v", err)
+ }
+ s.db = cloudDatastore{client: c}
+ }
+ s.log = log
+ return nil
+ })
+ if err != nil {
+ log.Fatalf("Error creating server: %v", err)
+ }
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8080"
+ }
+ log.Printf("Listening on :%v ...", port)
+ log.Fatalf("Error listening on :%v: %v", port, http.ListenAndServe(":"+port, s))
+}
+
+func projectID() string {
+ id := os.Getenv("DATASTORE_PROJECT_ID")
+ if id != "" {
+ return id
+ }
+ id, err := metadata.ProjectID()
+ if err != nil && os.Getenv("GAE_INSTANCE") != "" {
+ log.Fatalf("Could not determine the project ID: %v", err)
+ }
+ return id
+}
diff --git a/frontend/server.go b/frontend/server.go
new file mode 100644
index 0000000..881c6d7
--- /dev/null
+++ b/frontend/server.go
@@ -0,0 +1,81 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "golang.org/x/tools/godoc/static"
+)
+
+type server struct {
+ mux *http.ServeMux
+ db store
+ log logger
+
+ // When the executable was last modified. Used for caching headers of compiled assets.
+ modtime time.Time
+}
+
+func newServer(options ...func(s *server) error) (*server, error) {
+ s := &server{mux: http.NewServeMux()}
+ for _, o := range options {
+ if err := o(s); err != nil {
+ return nil, err
+ }
+ }
+ if s.db == nil {
+ return nil, fmt.Errorf("must provide an option func that specifies a datastore")
+ }
+ if s.log == nil {
+ return nil, fmt.Errorf("must provide an option func that specifies a logger")
+ }
+ execpath, _ := os.Executable()
+ if execpath != "" {
+ if fi, _ := os.Stat(execpath); fi != nil {
+ s.modtime = fi.ModTime()
+ }
+ }
+ s.init()
+ return s, nil
+}
+
+func (s *server) init() {
+ s.mux.HandleFunc("/", s.handleEdit)
+ s.mux.HandleFunc("/compile", s.handleCompile)
+ s.mux.HandleFunc("/fmt", handleFmt)
+ s.mux.HandleFunc("/share", s.handleShare)
+ s.mux.HandleFunc("/playground.js", s.handlePlaygroundJS)
+ s.mux.HandleFunc("/favicon.ico", handleFavicon)
+ s.mux.HandleFunc("/_ah/health", handleHealthCheck)
+
+ staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))
+ s.mux.Handle("/static/", staticHandler)
+}
+
+func (s *server) handlePlaygroundJS(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-type", "text/javascript; charset=utf-8")
+ rd := strings.NewReader(static.Files["playground.js"])
+ http.ServeContent(w, r, "playground.js", s.modtime, rd)
+}
+
+func handleFavicon(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./static/favicon.ico")
+}
+
+func handleHealthCheck(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "ok")
+}
+
+func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if os.Getenv("GAE_INSTANCE") != "" {
+ w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
+ }
+ s.mux.ServeHTTP(w, r)
+}
diff --git a/frontend/server_test.go b/frontend/server_test.go
new file mode 100644
index 0000000..19bc261
--- /dev/null
+++ b/frontend/server_test.go
@@ -0,0 +1,31 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+package main
+
+import "testing"
+
+type testLogger struct {
+ t *testing.T
+}
+
+func (l testLogger) Printf(format string, args ...interface{}) {
+ l.t.Logf(format, args...)
+}
+func (l testLogger) Errorf(format string, args ...interface{}) {
+ l.t.Errorf(format, args...)
+}
+func (l testLogger) Fatalf(format string, args ...interface{}) {
+ l.t.Fatalf(format, args...)
+}
+
+func testingOptions(t *testing.T) func(s *server) error {
+ return func(s *server) error {
+ s.db = &inMemStore{}
+ s.log = testLogger{t}
+ return nil
+ }
+}
+
+func TestEdit(t *testing.T) {
+}
diff --git a/frontend/share.go b/frontend/share.go
index 60c9a78..1ac139f 100644
--- a/frontend/share.go
+++ b/frontend/share.go
@@ -1,4 +1,4 @@
-// Copyright 2011 The Go Authors. All rights reserved.
+// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
@@ -12,8 +12,6 @@
"io"
"net/http"
"os"
-
- "cloud.google.com/go/datastore"
)
const salt = "[replace this with something unique]"
@@ -34,29 +32,27 @@
return string(b)[:10]
}
-func share(w http.ResponseWriter, r *http.Request) {
+func (s *server) handleShare(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method == "OPTIONS" {
// This is likely a pre-flight CORS request.
return
}
if r.Method != "POST" {
- status := http.StatusMethodNotAllowed
- http.Error(w, http.StatusText(status), status)
+ http.Error(w, "Requires POST", http.StatusMethodNotAllowed)
return
}
if !allowShare(r) {
- status := http.StatusUnavailableForLegalReasons
- http.Error(w, http.StatusText(status), status)
+ http.Error(w, "Either this isn't available in your country due to legal reasons, or our IP geolocation is wrong.",
+ http.StatusUnavailableForLegalReasons)
return
}
- ctx := r.Context()
var body bytes.Buffer
_, err := io.Copy(&body, io.LimitReader(r.Body, maxSnippetSize+1))
r.Body.Close()
if err != nil {
- fmt.Fprintf(os.Stderr, "reading Body: %v", err)
+ s.log.Errorf("reading Body: %v", err)
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
@@ -67,10 +63,8 @@
snip := &snippet{Body: body.Bytes()}
id := snip.ID()
- key := datastore.NameKey("Snippet", id, nil)
- _, err = datastoreClient.Put(ctx, key, snip)
- if err != nil {
- fmt.Fprintf(os.Stderr, "putting Snippet: %v", err)
+ if err := s.db.PutSnippet(r.Context(), id, snip); err != nil {
+ s.log.Errorf("putting Snippet: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
diff --git a/frontend/store.go b/frontend/store.go
new file mode 100644
index 0000000..3fecbd7
--- /dev/null
+++ b/frontend/store.go
@@ -0,0 +1,61 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "context"
+ "sync"
+
+ "cloud.google.com/go/datastore"
+)
+
+type store interface {
+ PutSnippet(ctx context.Context, id string, snip *snippet) error
+ GetSnippet(ctx context.Context, id string, snip *snippet) error
+}
+
+type cloudDatastore struct {
+ client *datastore.Client
+}
+
+func (s cloudDatastore) PutSnippet(ctx context.Context, id string, snip *snippet) error {
+ key := datastore.NameKey("Snippet", id, nil)
+ _, err := s.client.Put(ctx, key, snip)
+ return err
+}
+
+func (s cloudDatastore) GetSnippet(ctx context.Context, id string, snip *snippet) error {
+ key := datastore.NameKey("Snippet", id, nil)
+ return s.client.Get(ctx, key, snip)
+}
+
+// inMemStore is a store backed by a map that should only be used for testing.
+type inMemStore struct {
+ sync.RWMutex
+ m map[string]*snippet // key -> snippet
+}
+
+func (s *inMemStore) PutSnippet(_ context.Context, id string, snip *snippet) error {
+ s.Lock()
+ if s.m == nil {
+ s.m = map[string]*snippet{}
+ }
+ b := make([]byte, len(snip.Body))
+ copy(b, snip.Body)
+ s.m[id] = &snippet{Body: b}
+ s.Unlock()
+ return nil
+}
+
+func (s *inMemStore) GetSnippet(_ context.Context, id string, snip *snippet) error {
+ s.RLock()
+ defer s.RUnlock()
+ v, ok := s.m[id]
+ if !ok {
+ return datastore.ErrNoSuchEntity
+ }
+ *snip = *v
+ return nil
+}