diff options
author | Michael Muré <batolettre@gmail.com> | 2020-06-21 22:12:04 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2020-06-27 23:03:05 +0200 |
commit | 2ab6381a94d55fa22b80acdbb18849d6b24951f9 (patch) | |
tree | 99942b000955623ea7466b9fa4cc7dab37645df6 /api/http | |
parent | 5f72b04ef8e84b1c367ca6874519706318e351f5 (diff) | |
download | git-bug-2ab6381a94d55fa22b80acdbb18849d6b24951f9.tar.gz |
Reorganize the webUI and API code
Included in the changes:
- create a new /api root package to hold all API code, migrate /graphql in there
- git API handlers all use the cache instead of the repo directly
- git API handlers are now tested
- git API handlers now require a "repo" mux parameter
- lots of untangling of API/handlers/middleware
- less code in commands/webui.go
Diffstat (limited to 'api/http')
-rw-r--r-- | api/http/git_file_handler.go | 61 | ||||
-rw-r--r-- | api/http/git_file_handlers_test.go | 91 | ||||
-rw-r--r-- | api/http/git_file_upload_handler.go | 104 |
3 files changed, 256 insertions, 0 deletions
diff --git a/api/http/git_file_handler.go b/api/http/git_file_handler.go new file mode 100644 index 00000000..6bd6fa85 --- /dev/null +++ b/api/http/git_file_handler.go @@ -0,0 +1,61 @@ +package http + +import ( + "bytes" + "net/http" + "time" + + "github.com/gorilla/mux" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/util/git" +) + +// implement a http.Handler that will read and server git blob. +// +// Expected gorilla/mux parameters: +// - "repo" : the ref of the repo or "" for the default one +// - "hash" : the git hash of the file to retrieve +type gitFileHandler struct { + mrc *cache.MultiRepoCache +} + +func NewGitFileHandler(mrc *cache.MultiRepoCache) http.Handler { + return &gitFileHandler{mrc: mrc} +} + +func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var repo *cache.RepoCache + var err error + + repoVar := mux.Vars(r)["repo"] + switch repoVar { + case "": + repo, err = gfh.mrc.DefaultRepo() + default: + repo, err = gfh.mrc.ResolveRepo(repoVar) + } + + if err != nil { + http.Error(rw, "invalid repo reference", http.StatusBadRequest) + return + } + + hash := git.Hash(mux.Vars(r)["hash"]) + if !hash.IsValid() { + http.Error(rw, "invalid git hash", http.StatusBadRequest) + return + } + + // TODO: this mean that the whole file will he buffered in memory + // This can be a problem for big files. There might be a way around + // that by implementing a io.ReadSeeker that would read and discard + // data when a seek is called. + data, err := repo.ReadData(hash) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data)) +} diff --git a/api/http/git_file_handlers_test.go b/api/http/git_file_handlers_test.go new file mode 100644 index 00000000..81d97d61 --- /dev/null +++ b/api/http/git_file_handlers_test.go @@ -0,0 +1,91 @@ +package http + +import ( + "bytes" + "image" + "image/png" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" +) + +func TestGitFileHandlers(t *testing.T) { + repo := repository.CreateTestRepo(false) + defer repository.CleanupTestRepos(repo) + + mrc := cache.NewMultiRepoCache() + repoCache, err := mrc.RegisterDefaultRepository(repo) + require.NoError(t, err) + + author, err := repoCache.NewIdentity("test identity", "test@test.org") + require.NoError(t, err) + + err = repoCache.SetUserIdentity(author) + require.NoError(t, err) + + // UPLOAD + + uploadHandler := NewGitUploadFileHandler(mrc) + + img := image.NewNRGBA(image.Rect(0, 0, 50, 50)) + data := &bytes.Buffer{} + err = png.Encode(data, img) + require.NoError(t, err) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("uploadfile", "noname") + assert.NoError(t, err) + + _, err = part.Write(data.Bytes()) + assert.NoError(t, err) + + err = writer.Close() + assert.NoError(t, err) + + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", body) + r.Header.Add("Content-Type", writer.FormDataContentType()) + + // Simulate auth + r = r.WithContext(auth.CtxWithUser(r.Context(), author.Id())) + + // Handler's params + r = mux.SetURLVars(r, map[string]string{ + "repo": "", + }) + + uploadHandler.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, `{"hash":"3426a1488292d8f3f3c59ca679681336542b986f"}`, w.Body.String()) + // DOWNLOAD + + downloadHandler := NewGitFileHandler(mrc) + + w = httptest.NewRecorder() + r, _ = http.NewRequest("GET", "/", nil) + + // Simulate auth + r = r.WithContext(auth.CtxWithUser(r.Context(), author.Id())) + + // Handler's params + r = mux.SetURLVars(r, map[string]string{ + "repo": "", + "hash": "3426a1488292d8f3f3c59ca679681336542b986f", + }) + + downloadHandler.ServeHTTP(w, r) + assert.Equal(t, http.StatusOK, w.Code) + + assert.Equal(t, data.Bytes(), w.Body.Bytes()) +} diff --git a/api/http/git_file_upload_handler.go b/api/http/git_file_upload_handler.go new file mode 100644 index 00000000..1702b8b1 --- /dev/null +++ b/api/http/git_file_upload_handler.go @@ -0,0 +1,104 @@ +package http + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/cache" +) + +// implement a http.Handler that will accept and store content into git blob. +// +// Expected gorilla/mux parameters: +// - "repo" : the ref of the repo or "" for the default one +type gitUploadFileHandler struct { + mrc *cache.MultiRepoCache +} + +func NewGitUploadFileHandler(mrc *cache.MultiRepoCache) http.Handler { + return &gitUploadFileHandler{mrc: mrc} +} + +func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var repo *cache.RepoCache + var err error + + repoVar := mux.Vars(r)["repo"] + switch repoVar { + case "": + repo, err = gufh.mrc.DefaultRepo() + default: + repo, err = gufh.mrc.ResolveRepo(repoVar) + } + + if err != nil { + http.Error(rw, "invalid repo reference", http.StatusBadRequest) + return + } + + _, err = auth.UserFromCtx(r.Context(), repo) + if err == auth.ErrNotAuthenticated { + http.Error(rw, "read-only mode or not logged in", http.StatusForbidden) + return + } else if err != nil { + http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) + return + } + + // 100MB (github limit) + var maxUploadSize int64 = 100 * 1000 * 1000 + r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize) + if err := r.ParseMultipartForm(maxUploadSize); err != nil { + http.Error(rw, "file too big (100MB max)", http.StatusBadRequest) + return + } + + file, _, err := r.FormFile("uploadfile") + if err != nil { + http.Error(rw, "invalid file", http.StatusBadRequest) + return + } + defer file.Close() + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + http.Error(rw, "invalid file", http.StatusBadRequest) + return + } + + filetype := http.DetectContentType(fileBytes) + if filetype != "image/jpeg" && filetype != "image/jpg" && + filetype != "image/gif" && filetype != "image/png" { + http.Error(rw, "invalid file type", http.StatusBadRequest) + return + } + + hash, err := repo.StoreData(fileBytes) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + type response struct { + Hash string `json:"hash"` + } + + resp := response{Hash: string(hash)} + + js, err := json.Marshal(resp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/json") + _, err = rw.Write(js) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } +} |