webui.go

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