aboutsummaryrefslogtreecommitdiffstats
path: root/api/http
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2020-06-21 22:12:04 +0200
committerMichael Muré <batolettre@gmail.com>2020-06-27 23:03:05 +0200
commit2ab6381a94d55fa22b80acdbb18849d6b24951f9 (patch)
tree99942b000955623ea7466b9fa4cc7dab37645df6 /api/http
parent5f72b04ef8e84b1c367ca6874519706318e351f5 (diff)
downloadgit-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.go61
-rw-r--r--api/http/git_file_handlers_test.go91
-rw-r--r--api/http/git_file_upload_handler.go104
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
+ }
+}