webui.go

  1package commands
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"fmt"
  7	"github.com/MichaelMure/git-bug/graphql"
  8	"github.com/MichaelMure/git-bug/repository"
  9	"github.com/MichaelMure/git-bug/util"
 10	"github.com/MichaelMure/git-bug/webui"
 11	"github.com/gorilla/mux"
 12	"github.com/phayes/freeport"
 13	"github.com/skratchdot/open-golang/open"
 14	"github.com/spf13/cobra"
 15	"github.com/vektah/gqlgen/handler"
 16	"io/ioutil"
 17	"log"
 18	"net/http"
 19	"time"
 20)
 21
 22var port int
 23
 24func runWebUI(cmd *cobra.Command, args []string) error {
 25	if port == 0 {
 26		var err error
 27		port, err = freeport.GetFreePort()
 28		if err != nil {
 29			return err
 30		}
 31	}
 32
 33	addr := fmt.Sprintf("127.0.0.1:%d", port)
 34	webUiAddr := fmt.Sprintf("http://%s", addr)
 35
 36	fmt.Printf("Web UI: %s\n", webUiAddr)
 37	fmt.Printf("Graphql API: http://%s/graphql\n", addr)
 38	fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
 39
 40	router := mux.NewRouter()
 41
 42	// Routes
 43	router.Path("/playground").Handler(handler.Playground("git-bug", "/graphql"))
 44	router.Path("/graphql").Handler(graphql.NewHandler(repo))
 45	router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo))
 46	router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo))
 47	router.PathPrefix("/").Handler(http.FileServer(webui.WebUIAssets))
 48
 49	open.Run(webUiAddr)
 50
 51	log.Fatal(http.ListenAndServe(addr, router))
 52
 53	return nil
 54}
 55
 56type gitFileHandler struct {
 57	repo repository.Repo
 58}
 59
 60func newGitFileHandler(repo repository.Repo) http.Handler {
 61	return &gitFileHandler{
 62		repo: repo,
 63	}
 64}
 65
 66func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 67	hash := mux.Vars(r)["hash"]
 68
 69	if !isGitHash(hash) {
 70		http.Error(rw, "invalid git hash", http.StatusBadRequest)
 71		return
 72	}
 73
 74	// TODO: this mean that the whole file will he buffered in memory
 75	// This can be a problem for big files. There might be a way around
 76	// that by implementing a io.ReadSeeker that would read and discard
 77	// data when a seek is called.
 78	data, err := gfh.repo.ReadData(util.Hash(hash))
 79	if err != nil {
 80		http.Error(rw, err.Error(), http.StatusInternalServerError)
 81		return
 82	}
 83
 84	http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data))
 85}
 86
 87type gitUploadFileHandler struct {
 88	repo repository.Repo
 89}
 90
 91func newGitUploadFileHandler(repo repository.Repo) http.Handler {
 92	return &gitUploadFileHandler{
 93		repo: repo,
 94	}
 95}
 96
 97func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 98	// 100MB (github limit)
 99	var maxUploadSize int64 = 100 * 1000 * 1000
100	r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize)
101	if err := r.ParseMultipartForm(maxUploadSize); err != nil {
102		http.Error(rw, "file too big (100MB max)", http.StatusBadRequest)
103		return
104	}
105
106	file, _, err := r.FormFile("uploadfile")
107	if err != nil {
108		http.Error(rw, "invalid file", http.StatusBadRequest)
109		return
110	}
111	defer file.Close()
112	fileBytes, err := ioutil.ReadAll(file)
113	if err != nil {
114		http.Error(rw, "invalid file", http.StatusBadRequest)
115		return
116	}
117
118	filetype := http.DetectContentType(fileBytes)
119	if filetype != "image/jpeg" && filetype != "image/jpg" &&
120		filetype != "image/gif" && filetype != "image/png" {
121		http.Error(rw, "invalid file type", http.StatusBadRequest)
122		return
123	}
124
125	hash, err := gufh.repo.StoreData(fileBytes)
126	if err != nil {
127		http.Error(rw, err.Error(), http.StatusInternalServerError)
128		return
129	}
130
131	type response struct {
132		Hash string `json:"hash"`
133	}
134
135	resp := response{Hash: string(hash)}
136
137	js, err := json.Marshal(resp)
138	if err != nil {
139		http.Error(rw, err.Error(), http.StatusInternalServerError)
140		return
141	}
142
143	rw.Header().Set("Content-Type", "application/json")
144	rw.Write(js)
145}
146
147func isGitHash(s string) bool {
148	if len(s) != 40 {
149		return false
150	}
151	for _, r := range s {
152		if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
153			return false
154		}
155	}
156	return true
157}
158
159var webUICmd = &cobra.Command{
160	Use:   "webui",
161	Short: "Launch the web UI",
162	RunE:  runWebUI,
163}
164
165func init() {
166	RootCmd.AddCommand(webUICmd)
167	webUICmd.Flags().IntVarP(&port, "port", "p", 0, "Port to listen to")
168}