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 // 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 fmt.Println("Press Ctrl+c to quit")
90
91 err = open.Run(webUiAddr)
92 if err != nil {
93 fmt.Println(err)
94 }
95
96 err = srv.ListenAndServe()
97 if err != nil && err != http.ErrServerClosed {
98 return err
99 }
100
101 <-done
102
103 fmt.Println("WebUI stopped")
104 return nil
105}
106
107type gitFileHandler struct {
108 repo repository.Repo
109}
110
111func newGitFileHandler(repo repository.Repo) http.Handler {
112 return &gitFileHandler{
113 repo: repo,
114 }
115}
116
117func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
118 hash := git.Hash(mux.Vars(r)["hash"])
119
120 if !hash.IsValid() {
121 http.Error(rw, "invalid git hash", http.StatusBadRequest)
122 return
123 }
124
125 // TODO: this mean that the whole file will he buffered in memory
126 // This can be a problem for big files. There might be a way around
127 // that by implementing a io.ReadSeeker that would read and discard
128 // data when a seek is called.
129 data, err := gfh.repo.ReadData(git.Hash(hash))
130 if err != nil {
131 http.Error(rw, err.Error(), http.StatusInternalServerError)
132 return
133 }
134
135 http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data))
136}
137
138type gitUploadFileHandler struct {
139 repo repository.Repo
140}
141
142func newGitUploadFileHandler(repo repository.Repo) http.Handler {
143 return &gitUploadFileHandler{
144 repo: repo,
145 }
146}
147
148func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
149 // 100MB (github limit)
150 var maxUploadSize int64 = 100 * 1000 * 1000
151 r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize)
152 if err := r.ParseMultipartForm(maxUploadSize); err != nil {
153 http.Error(rw, "file too big (100MB max)", http.StatusBadRequest)
154 return
155 }
156
157 file, _, err := r.FormFile("uploadfile")
158 if err != nil {
159 http.Error(rw, "invalid file", http.StatusBadRequest)
160 return
161 }
162 defer file.Close()
163 fileBytes, err := ioutil.ReadAll(file)
164 if err != nil {
165 http.Error(rw, "invalid file", http.StatusBadRequest)
166 return
167 }
168
169 filetype := http.DetectContentType(fileBytes)
170 if filetype != "image/jpeg" && filetype != "image/jpg" &&
171 filetype != "image/gif" && filetype != "image/png" {
172 http.Error(rw, "invalid file type", http.StatusBadRequest)
173 return
174 }
175
176 hash, err := gufh.repo.StoreData(fileBytes)
177 if err != nil {
178 http.Error(rw, err.Error(), http.StatusInternalServerError)
179 return
180 }
181
182 type response struct {
183 Hash string `json:"hash"`
184 }
185
186 resp := response{Hash: string(hash)}
187
188 js, err := json.Marshal(resp)
189 if err != nil {
190 http.Error(rw, err.Error(), http.StatusInternalServerError)
191 return
192 }
193
194 rw.Header().Set("Content-Type", "application/json")
195 _, err = rw.Write(js)
196 if err != nil {
197 http.Error(rw, err.Error(), http.StatusInternalServerError)
198 return
199 }
200}
201
202var webUICmd = &cobra.Command{
203 Use: "webui",
204 Short: "Launch the web UI",
205 RunE: runWebUI,
206}
207
208func init() {
209 RootCmd.AddCommand(webUICmd)
210
211 webUICmd.Flags().SortFlags = false
212
213 webUICmd.Flags().IntVarP(&port, "port", "p", 0, "Port to listen to")
214}