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/99designs/gqlgen/handler"
 16	"github.com/MichaelMure/git-bug/graphql"
 17	"github.com/MichaelMure/git-bug/repository"
 18	"github.com/MichaelMure/git-bug/util/git"
 19	"github.com/MichaelMure/git-bug/webui"
 20	"github.com/gorilla/mux"
 21	"github.com/phayes/freeport"
 22	"github.com/skratchdot/open-golang/open"
 23	"github.com/spf13/cobra"
 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	assetsHandler := &fileSystemWithDefault{
 48		FileSystem:  webui.WebUIAssets,
 49		defaultFile: "index.html",
 50	}
 51
 52	// Routes
 53	router.Path("/playground").Handler(handler.Playground("git-bug", "/graphql"))
 54	router.Path("/graphql").Handler(graphqlHandler)
 55	router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo))
 56	router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo))
 57	router.PathPrefix("/").Handler(http.FileServer(assetsHandler))
 58
 59	srv := &http.Server{
 60		Addr:    addr,
 61		Handler: router,
 62	}
 63
 64	done := make(chan bool)
 65	quit := make(chan os.Signal, 1)
 66
 67	// register as handler of the interrupt signal to trigger the teardown
 68	signal.Notify(quit, os.Interrupt)
 69
 70	go func() {
 71		<-quit
 72		fmt.Println("WebUI is shutting down...")
 73
 74		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 75		defer cancel()
 76
 77		srv.SetKeepAlivesEnabled(false)
 78		if err := srv.Shutdown(ctx); err != nil {
 79			log.Fatalf("Could not gracefully shutdown the WebUI: %v\n", err)
 80		}
 81
 82		// Teardown
 83		err := graphqlHandler.Close()
 84		if err != nil {
 85			fmt.Println(err)
 86		}
 87
 88		close(done)
 89	}()
 90
 91	fmt.Printf("Web UI: %s\n", webUiAddr)
 92	fmt.Printf("Graphql API: http://%s/graphql\n", addr)
 93	fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
 94	fmt.Println("Press Ctrl+c to quit")
 95
 96	err = open.Run(webUiAddr)
 97	if err != nil {
 98		fmt.Println(err)
 99	}
100
101	err = srv.ListenAndServe()
102	if err != nil && err != http.ErrServerClosed {
103		return err
104	}
105
106	<-done
107
108	fmt.Println("WebUI stopped")
109	return nil
110}
111
112// implement a http.FileSystem that will serve a default file when the looked up
113// file doesn't exist. Useful for Single-Page App that implement routing client
114// side, where the server has to return the root index.html file for every route.
115type fileSystemWithDefault struct {
116	http.FileSystem
117	defaultFile string
118}
119
120func (fswd *fileSystemWithDefault) Open(name string) (http.File, error) {
121	f, err := fswd.FileSystem.Open(name)
122	if os.IsNotExist(err) {
123		return fswd.FileSystem.Open(fswd.defaultFile)
124	}
125	return f, err
126}
127
128// implement a http.Handler that will read and server git blob.
129type gitFileHandler struct {
130	repo repository.Repo
131}
132
133func newGitFileHandler(repo repository.Repo) http.Handler {
134	return &gitFileHandler{
135		repo: repo,
136	}
137}
138
139func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
140	hash := git.Hash(mux.Vars(r)["hash"])
141
142	if !hash.IsValid() {
143		http.Error(rw, "invalid git hash", http.StatusBadRequest)
144		return
145	}
146
147	// TODO: this mean that the whole file will he buffered in memory
148	// This can be a problem for big files. There might be a way around
149	// that by implementing a io.ReadSeeker that would read and discard
150	// data when a seek is called.
151	data, err := gfh.repo.ReadData(git.Hash(hash))
152	if err != nil {
153		http.Error(rw, err.Error(), http.StatusInternalServerError)
154		return
155	}
156
157	http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data))
158}
159
160// implement a http.Handler that will accept and store content into git blob.
161type gitUploadFileHandler struct {
162	repo repository.Repo
163}
164
165func newGitUploadFileHandler(repo repository.Repo) http.Handler {
166	return &gitUploadFileHandler{
167		repo: repo,
168	}
169}
170
171func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
172	// 100MB (github limit)
173	var maxUploadSize int64 = 100 * 1000 * 1000
174	r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize)
175	if err := r.ParseMultipartForm(maxUploadSize); err != nil {
176		http.Error(rw, "file too big (100MB max)", http.StatusBadRequest)
177		return
178	}
179
180	file, _, err := r.FormFile("uploadfile")
181	if err != nil {
182		http.Error(rw, "invalid file", http.StatusBadRequest)
183		return
184	}
185	defer file.Close()
186	fileBytes, err := ioutil.ReadAll(file)
187	if err != nil {
188		http.Error(rw, "invalid file", http.StatusBadRequest)
189		return
190	}
191
192	filetype := http.DetectContentType(fileBytes)
193	if filetype != "image/jpeg" && filetype != "image/jpg" &&
194		filetype != "image/gif" && filetype != "image/png" {
195		http.Error(rw, "invalid file type", http.StatusBadRequest)
196		return
197	}
198
199	hash, err := gufh.repo.StoreData(fileBytes)
200	if err != nil {
201		http.Error(rw, err.Error(), http.StatusInternalServerError)
202		return
203	}
204
205	type response struct {
206		Hash string `json:"hash"`
207	}
208
209	resp := response{Hash: string(hash)}
210
211	js, err := json.Marshal(resp)
212	if err != nil {
213		http.Error(rw, err.Error(), http.StatusInternalServerError)
214		return
215	}
216
217	rw.Header().Set("Content-Type", "application/json")
218	_, err = rw.Write(js)
219	if err != nil {
220		http.Error(rw, err.Error(), http.StatusInternalServerError)
221		return
222	}
223}
224
225var webUICmd = &cobra.Command{
226	Use:     "webui",
227	Short:   "Launch the web UI.",
228	PreRunE: loadRepo,
229	RunE:    runWebUI,
230}
231
232func init() {
233	RootCmd.AddCommand(webUICmd)
234
235	webUICmd.Flags().SortFlags = false
236
237	webUICmd.Flags().IntVarP(&port, "port", "p", 0, "Port to listen to")
238}