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}