Skip to content
Permalink
Browse files

refactor top-level code organization

Make changes to improve readability and modernize top-level
code organization.

This change eleminates global variables to help readability.
There is now a snippet store abstraction, and an HTTP server
abstraction. Both are backed by struct types with methods.
As a result, it's easier to see all of the dependencies for
each component of code.
  • Loading branch information...
dmitshur committed Jun 22, 2019
1 parent 31a11ab commit 537165a38cf1200cb25592ec46dc787755e8d269
Showing with 109 additions and 72 deletions.
  1. +61 −51 main.go
  2. +48 −21 store.go
112 main.go
@@ -18,52 +18,52 @@ import (
"golang.org/x/net/webdav"
)

var storageDirFlag = flag.String("storage-dir", "", "Storage dir for snippets; if empty, a volatile in-memory store is used.")
var httpFlag = flag.String("http", ":8080", "Listen for HTTP connections on this address.")
var allowOriginFlag = flag.String("allow-origin", "http://www.gopherjs.org", "Access-Control-Allow-Origin header value.")
var (
storageDirFlag = flag.String("storage-dir", "", "Storage dir for snippets; if empty, a volatile in-memory store is used.")
httpFlag = flag.String("http", ":8080", "Listen for HTTP connections on this address.")
allowOriginFlag = flag.String("allow-origin", "http://www.gopherjs.org", "Access-Control-Allow-Origin header value.")
)

const maxSnippetSizeBytes = 1024 * 1024

func pHandler(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", *allowOriginFlag)

if req.Method != "GET" {
w.Header().Set("Allow", "GET")
http.Error(w, "Method should be GET.", http.StatusMethodNotAllowed)
return
}
func main() {
flag.Parse()

id := req.URL.Path[len("/p/"):]
err := validateID(id)
if err != nil {
http.Error(w, "Unexpected id format.", http.StatusBadRequest)
return
var localStore webdav.FileSystem
switch *storageDirFlag {
default:
err := os.MkdirAll(*storageDirFlag, 0755)
if err != nil {
log.Fatalf("error creating directory %q: %v", *storageDirFlag, err)
}
localStore = webdav.Dir(*storageDirFlag)
case "":
localStore = webdav.NewMemFS()
}

var snippet io.Reader
if rc, err := getSnippetFromLocalStore(req.Context(), id); err == nil { // Check if we have the snippet locally first.
defer rc.Close()
snippet = rc
} else if rc, err = getSnippetFromGoPlayground(req.Context(), id); err == nil { // If not found locally, try the Go Playground.
defer rc.Close()
snippet = rc
s := &Server{
Store: &Store{
LocalFS: localStore,
},
}
http.HandleFunc("/share", s.ShareHandler) // "/share", save snippet and return its id.
http.HandleFunc("/p/", s.PHandler) // "/p/{{.SnippetId}}", serve snippet by id.

if snippet == nil {
// Snippet not found.
http.Error(w, "Snippet not found.", http.StatusNotFound)
return
}
log.Println("Started.")

_, err = io.Copy(w, snippet)
err := http.ListenAndServe(*httpFlag, nil)
if err != nil {
log.Println(err)
http.Error(w, "Server error.", http.StatusInternalServerError)
return
log.Fatalln("ListenAndServe:", err)
}
}

func shareHandler(w http.ResponseWriter, req *http.Request) {
// Server is the snippet store HTTP server.
type Server struct {
Store *Store
}

// ShareHandler is the handler for "/share" requests.
func (s *Server) ShareHandler(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", *allowOriginFlag)
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") // Needed for Safari.

@@ -80,7 +80,7 @@ func shareHandler(w http.ResponseWriter, req *http.Request) {
return
}

id, err := storeSnippet(req.Context(), body)
id, err := s.Store.StoreSnippet(req.Context(), body)
if err != nil {
log.Println(err)
http.Error(w, "Server error.", http.StatusInternalServerError)
@@ -90,32 +90,42 @@ func shareHandler(w http.ResponseWriter, req *http.Request) {
_, err = io.WriteString(w, id)
if err != nil {
log.Println(err)
http.Error(w, "Server error.", http.StatusInternalServerError)
return
}
}

func main() {
flag.Parse()
// PHandler is the handler for "/p/{{.SnippetId}}" requests.
func (s *Server) PHandler(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", *allowOriginFlag)

switch *storageDirFlag {
default:
err := os.MkdirAll(*storageDirFlag, 0755)
if err != nil {
log.Fatalf("Error creating directory %q: %v.\n", *storageDirFlag, err)
}
localStore = webdav.Dir(*storageDirFlag)
case "":
localStore = webdav.NewMemFS()
if req.Method != "GET" {
w.Header().Set("Allow", "GET")
http.Error(w, "Method should be GET.", http.StatusMethodNotAllowed)
return
}

http.HandleFunc("/p/", pHandler) // "/p/{{.SnippetId}}", serve snippet by id.
http.HandleFunc("/share", shareHandler) // "/share", save snippet and return its id.
id := req.URL.Path[len("/p/"):]
err := validateID(id)
if err != nil {
http.Error(w, "Unexpected id format.", http.StatusBadRequest)
return
}

log.Println("Started.")
snippet, err := s.Store.LoadSnippet(req.Context(), id)
if os.IsNotExist(err) {
// Snippet not found.
http.Error(w, "Snippet not found.", http.StatusNotFound)
return
} else if err != nil {
log.Println(err)
http.Error(w, "Server error.", http.StatusInternalServerError)
return
}
defer snippet.Close()

err := http.ListenAndServe(*httpFlag, nil)
_, err = io.Copy(w, snippet)
if err != nil {
log.Fatalln("ListenAndServe:", err)
log.Println(err)
return
}
}
@@ -5,48 +5,75 @@ import (
"fmt"
"io"
"net/http"
"os"

"github.com/shurcooL/webdavfs/vfsutil"
"golang.org/x/net/webdav"
)

// localStore is the local store for snippets. Snippets are kept as files in root directory.
var localStore webdav.FileSystem
const userAgent = "gopherjs.org/play/ playground snippet fetcher"

// getSnippetFromLocalStore tries to get the snippet with given id from local store.
// If it returns nil error, the ReadCloser must be closed by caller.
func getSnippetFromLocalStore(ctx context.Context, id string) (io.ReadCloser, error) {
return vfsutil.Open(ctx, localStore, id)
// Store is the snippet store.
type Store struct {
// LocalFS is the local store for snippets. Snippets are kept as files,
// named after the snippet id (with no extension), in the root directory.
LocalFS webdav.FileSystem
}

const userAgent = "gopherjs.org/play/ playground snippet fetcher"
// StoreSnippet stores the provided snippet,
// and returns the id assigned to the snippet.
func (s *Store) StoreSnippet(ctx context.Context, body []byte) (id string, err error) {
// Store the snippet locally.
id = snippetBodyToID(body)
err = vfsutil.WriteFile(ctx, s.LocalFS, id, body, 0644)
return id, err
}

// getSnippetFromGoPlayground tries to get the snippet with given id from the Go Playground.
// LoadSnippet loads the snippet with given id.
// It first tries the local store, then the Go Playground.
//
// It returns an error that satisfies os.IsNotExist if snippet is not found.
// If it returns nil error, the ReadCloser must be closed by caller.
func getSnippetFromGoPlayground(ctx context.Context, id string) (io.ReadCloser, error) {
func (s *Store) LoadSnippet(ctx context.Context, id string) (io.ReadCloser, error) {
// Check if we have the snippet locally first.
if snippet, err := vfsutil.Open(ctx, s.LocalFS, id); err == nil {
return snippet, nil
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("loadSnippetFromLocalStore: %v", err)
}

// If not found locally, try the Go Playground.
if snippet, err := fetchSnippetFromGoPlayground(ctx, id); err == nil {
return snippet, nil
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("fetchSnippetFromGoPlayground: %v", err)
}

// Not found in either place.
return nil, os.ErrNotExist
}

// fetchSnippetFromGoPlayground fetches the snippet with given id
// from the Go Playground.
//
// It returns an error that satisfies os.IsNotExist if snippet is not found.
// If it returns nil error, the ReadCloser must be closed by caller.
func fetchSnippetFromGoPlayground(ctx context.Context, id string) (io.ReadCloser, error) {
req, err := http.NewRequest("GET", "https://play.golang.org/p/"+id+".go", nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)

resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
resp.Body.Close()
return nil, os.ErrNotExist
} else if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("Go Playground returned unexpected status code %v", resp.StatusCode)
}

return resp.Body, nil
}

// storeSnippet stores snippet in local storage.
// It returns the id assigned to the snippet.
func storeSnippet(ctx context.Context, body []byte) (id string, err error) {
id = snippetBodyToID(body)
err = vfsutil.WriteFile(ctx, localStore, id, body, 0644)
return id, err
}

0 comments on commit 537165a

Please sign in to comment.
You can’t perform that action at this time.