diff options
Diffstat (limited to 'internal/route')
-rw-r--r-- | internal/route/lfs/basic.go | 60 | ||||
-rw-r--r-- | internal/route/lfs/basic_test.go | 288 | ||||
-rw-r--r-- | internal/route/lfs/batch_test.go | 1 | ||||
-rw-r--r-- | internal/route/lfs/main_test.go | 30 | ||||
-rw-r--r-- | internal/route/lfs/route.go | 14 |
5 files changed, 375 insertions, 18 deletions
diff --git a/internal/route/lfs/basic.go b/internal/route/lfs/basic.go index f0c2dc8b..626f5ff0 100644 --- a/internal/route/lfs/basic.go +++ b/internal/route/lfs/basic.go @@ -9,13 +9,11 @@ import ( "io" "io/ioutil" "net/http" - "os" "strconv" "gopkg.in/macaron.v1" log "unknwon.dev/clog/v2" - "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/db" "gogs.io/gogs/internal/lfsutil" "gogs.io/gogs/internal/strutil" @@ -27,8 +25,25 @@ const ( basicOperationDownload = "download" ) +type basicHandler struct { + // The default storage backend for uploading new objects. + defaultStorage lfsutil.Storage + // The list of available storage backends to access objects. + storagers map[lfsutil.Storage]lfsutil.Storager +} + +// DefaultStorager returns the default storage backend. +func (h *basicHandler) DefaultStorager() lfsutil.Storager { + return h.storagers[h.defaultStorage] +} + +// Storager returns the given storage backend. +func (h *basicHandler) Storager(storage lfsutil.Storage) lfsutil.Storager { + return h.storagers[storage] +} + // GET /{owner}/{repo}.git/info/lfs/object/basic/{oid} -func serveBasicDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) { +func (h *basicHandler) serveDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) { object, err := db.LFS.GetObjectByOID(repo.ID, oid) if err != nil { if db.IsErrLFSObjectNotExist(err) { @@ -42,28 +57,26 @@ func serveBasicDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID return } - fpath := lfsutil.StorageLocalPath(conf.LFS.ObjectsPath, object.OID) - r, err := os.Open(fpath) - if err != nil { + s := h.Storager(object.Storage) + if s == nil { internalServerError(c.Resp) - log.Error("Failed to open object file [path: %s]: %v", fpath, err) + log.Error("Failed to locate the object [repo_id: %d, oid: %s]: storage %q not found", object.RepoID, object.OID, object.Storage) return } - defer r.Close() c.Header().Set("Content-Type", "application/octet-stream") c.Header().Set("Content-Length", strconv.FormatInt(object.Size, 10)) c.Status(http.StatusOK) - _, err = io.Copy(c.Resp, r) + err = s.Download(object.OID, c.Resp) if err != nil { - log.Error("Failed to copy object file: %v", err) + log.Error("Failed to download object [oid: %s]: %v", object.OID, err) return } } // PUT /{owner}/{repo}.git/info/lfs/object/basic/{oid} -func serveBasicUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) { +func (h *basicHandler) serveUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) { // NOTE: LFS client will retry upload the same object if there was a partial failure, // therefore we would like to skip ones that already exist. _, err := db.LFS.GetObjectByOID(repo.ID, oid) @@ -79,8 +92,25 @@ func serveBasicUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) return } - err = db.LFS.CreateObject(repo.ID, oid, c.Req.Request.Body, lfsutil.StorageLocal) + s := h.DefaultStorager() + written, err := s.Upload(oid, c.Req.Request.Body) + if err != nil { + if err == lfsutil.ErrInvalidOID { + responseJSON(c.Resp, http.StatusBadRequest, responseError{ + Message: err.Error(), + }) + } else { + internalServerError(c.Resp) + log.Error("Failed to upload object [storage: %s, oid: %s]: %v", s.Storage(), oid, err) + } + return + } + + err = db.LFS.CreateObject(repo.ID, oid, written, s.Storage()) if err != nil { + // NOTE: It is OK to leave the file when the whole operation failed + // with a DB error, a retry on client side can safely overwrite the + // same file as OID is seen as unique to every file. internalServerError(c.Resp) log.Error("Failed to create object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err) return @@ -91,7 +121,7 @@ func serveBasicUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) } // POST /{owner}/{repo}.git/info/lfs/object/basic/verify -func serveBasicVerify(c *macaron.Context, repo *db.Repository) { +func (h *basicHandler) serveVerify(c *macaron.Context, repo *db.Repository) { var request basicVerifyRequest defer c.Req.Request.Body.Close() err := json.NewDecoder(c.Req.Request.Body).Decode(&request) @@ -109,7 +139,7 @@ func serveBasicVerify(c *macaron.Context, repo *db.Repository) { return } - object, err := db.LFS.GetObjectByOID(repo.ID, lfsutil.OID(request.Oid)) + object, err := db.LFS.GetObjectByOID(repo.ID, request.Oid) if err != nil { if db.IsErrLFSObjectNotExist(err) { responseJSON(c.Resp, http.StatusNotFound, responseError{ @@ -123,7 +153,7 @@ func serveBasicVerify(c *macaron.Context, repo *db.Repository) { } if object.Size != request.Size { - responseJSON(c.Resp, http.StatusNotFound, responseError{ + responseJSON(c.Resp, http.StatusBadRequest, responseError{ Message: "Object size mismatch", }) return diff --git a/internal/route/lfs/basic_test.go b/internal/route/lfs/basic_test.go new file mode 100644 index 00000000..db2fbe61 --- /dev/null +++ b/internal/route/lfs/basic_test.go @@ -0,0 +1,288 @@ +// Copyright 2020 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package lfs + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/macaron.v1" + + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/lfsutil" +) + +var _ lfsutil.Storager = (*mockStorage)(nil) + +// mockStorage is a in-memory storage for LFS objects. +type mockStorage struct { + buf *bytes.Buffer +} + +func (s *mockStorage) Storage() lfsutil.Storage { + return "memory" +} + +func (s *mockStorage) Upload(oid lfsutil.OID, rc io.ReadCloser) (int64, error) { + defer rc.Close() + return io.Copy(s.buf, rc) +} + +func (s *mockStorage) Download(oid lfsutil.OID, w io.Writer) error { + _, err := io.Copy(w, s.buf) + return err +} + +func Test_basicHandler_serveDownload(t *testing.T) { + s := &mockStorage{} + basic := &basicHandler{ + defaultStorage: s.Storage(), + storagers: map[lfsutil.Storage]lfsutil.Storager{ + s.Storage(): s, + }, + } + + m := macaron.New() + m.Use(macaron.Renderer()) + m.Use(func(c *macaron.Context) { + c.Map(&db.Repository{Name: "repo"}) + c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f")) + }) + m.Get("/", basic.serveDownload) + + tests := []struct { + name string + content string + mockLFSStore *db.MockLFSStore + expStatusCode int + expHeader http.Header + expBody string + }{ + { + name: "object does not exist", + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return nil, db.ErrLFSObjectNotExist{} + }, + }, + expStatusCode: http.StatusNotFound, + expHeader: http.Header{ + "Content-Type": []string{"application/vnd.git-lfs+json"}, + }, + expBody: `{"message":"Object does not exist"}` + "\n", + }, + { + name: "storage not found", + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return &db.LFSObject{Storage: "bad_storage"}, nil + }, + }, + expStatusCode: http.StatusInternalServerError, + expHeader: http.Header{ + "Content-Type": []string{"application/vnd.git-lfs+json"}, + }, + expBody: `{"message":"Internal server error"}` + "\n", + }, + + { + name: "object exists", + content: "Hello world!", + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return &db.LFSObject{ + Size: 12, + Storage: s.Storage(), + }, nil + }, + }, + expStatusCode: http.StatusOK, + expHeader: http.Header{ + "Content-Type": []string{"application/octet-stream"}, + "Content-Length": []string{"12"}, + }, + expBody: "Hello world!", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db.SetMockLFSStore(t, test.mockLFSStore) + + s.buf = bytes.NewBufferString(test.content) + + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + m.ServeHTTP(rr, r) + + resp := rr.Result() + assert.Equal(t, test.expStatusCode, resp.StatusCode) + assert.Equal(t, test.expHeader, resp.Header) + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, test.expBody, string(body)) + }) + } +} + +func Test_basicHandler_serveUpload(t *testing.T) { + s := &mockStorage{buf: &bytes.Buffer{}} + basic := &basicHandler{ + defaultStorage: s.Storage(), + storagers: map[lfsutil.Storage]lfsutil.Storager{ + s.Storage(): s, + }, + } + + m := macaron.New() + m.Use(macaron.Renderer()) + m.Use(func(c *macaron.Context) { + c.Map(&db.Repository{Name: "repo"}) + c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f")) + }) + m.Put("/", basic.serveUpload) + + tests := []struct { + name string + mockLFSStore *db.MockLFSStore + expStatusCode int + expBody string + }{ + { + name: "object already exists", + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return &db.LFSObject{}, nil + }, + }, + expStatusCode: http.StatusOK, + }, + { + name: "new object", + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return nil, db.ErrLFSObjectNotExist{} + }, + MockCreateObject: func(repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error { + return nil + }, + }, + expStatusCode: http.StatusOK, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db.SetMockLFSStore(t, test.mockLFSStore) + + r, err := http.NewRequest("PUT", "/", strings.NewReader("Hello world!")) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + m.ServeHTTP(rr, r) + + resp := rr.Result() + assert.Equal(t, test.expStatusCode, resp.StatusCode) + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, test.expBody, string(body)) + }) + } +} + +func Test_basicHandler_serveVerify(t *testing.T) { + m := macaron.New() + m.Use(macaron.Renderer()) + m.Use(func(c *macaron.Context) { + c.Map(&db.Repository{Name: "repo"}) + }) + m.Post("/", (&basicHandler{}).serveVerify) + + tests := []struct { + name string + body string + mockLFSStore *db.MockLFSStore + expStatusCode int + expBody string + }{ + { + name: "invalid oid", + body: `{"oid": "bad_oid"}`, + expStatusCode: http.StatusBadRequest, + expBody: `{"message":"Invalid oid"}` + "\n", + }, + { + name: "object does not exist", + body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`, + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return nil, db.ErrLFSObjectNotExist{} + }, + }, + expStatusCode: http.StatusNotFound, + expBody: `{"message":"Object does not exist"}` + "\n", + }, + { + name: "object size mismatch", + body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`, + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return &db.LFSObject{Size: 12}, nil + }, + }, + expStatusCode: http.StatusBadRequest, + expBody: `{"message":"Object size mismatch"}` + "\n", + }, + + { + name: "object exists", + body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size":12}`, + mockLFSStore: &db.MockLFSStore{ + MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) { + return &db.LFSObject{Size: 12}, nil + }, + }, + expStatusCode: http.StatusOK, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db.SetMockLFSStore(t, test.mockLFSStore) + + r, err := http.NewRequest("POST", "/", strings.NewReader(test.body)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + m.ServeHTTP(rr, r) + + resp := rr.Result() + assert.Equal(t, test.expStatusCode, resp.StatusCode) + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, test.expBody, string(body)) + }) + } +} diff --git a/internal/route/lfs/batch_test.go b/internal/route/lfs/batch_test.go index 7dce2004..61cfb562 100644 --- a/internal/route/lfs/batch_test.go +++ b/internal/route/lfs/batch_test.go @@ -23,6 +23,7 @@ func Test_serveBatch(t *testing.T) { conf.SetMockServer(t, conf.ServerOpts{ ExternalURL: "https://gogs.example.com/", }) + m := macaron.New() m.Use(func(c *macaron.Context) { c.Map(&db.User{Name: "owner"}) diff --git a/internal/route/lfs/main_test.go b/internal/route/lfs/main_test.go new file mode 100644 index 00000000..ddc04c1b --- /dev/null +++ b/internal/route/lfs/main_test.go @@ -0,0 +1,30 @@ +// Copyright 2020 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package lfs + +import ( + "flag" + "fmt" + "os" + "testing" + + log "unknwon.dev/clog/v2" + + "gogs.io/gogs/internal/testutil" +) + +func TestMain(m *testing.M) { + flag.Parse() + if !testing.Verbose() { + // Remove the primary logger and register a noop logger. + log.Remove(log.DefaultConsoleName) + err := log.New("noop", testutil.InitNoopLogger) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + os.Exit(m.Run()) +} diff --git a/internal/route/lfs/route.go b/internal/route/lfs/route.go index 08045ac5..16478803 100644 --- a/internal/route/lfs/route.go +++ b/internal/route/lfs/route.go @@ -13,6 +13,7 @@ import ( log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/authutil" + "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/db" "gogs.io/gogs/internal/lfsutil" ) @@ -26,10 +27,17 @@ func RegisterRoutes(r *macaron.Router) { r.Group("", func() { r.Post("/objects/batch", authorize(db.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch) r.Group("/objects/basic", func() { + + basic := &basicHandler{ + defaultStorage: lfsutil.Storage(conf.LFS.Storage), + storagers: map[lfsutil.Storage]lfsutil.Storager{ + lfsutil.StorageLocal: &lfsutil.LocalStorage{Root: conf.LFS.ObjectsPath}, + }, + } r.Combo("/:oid", verifyOID()). - Get(authorize(db.AccessModeRead), serveBasicDownload). - Put(authorize(db.AccessModeWrite), verifyContentTypeStream, serveBasicUpload) - r.Post("/verify", authorize(db.AccessModeWrite), verifyAccept, verifyContentTypeJSON, serveBasicVerify) + Get(authorize(db.AccessModeRead), basic.serveDownload). + Put(authorize(db.AccessModeWrite), verifyContentTypeStream, basic.serveUpload) + r.Post("/verify", authorize(db.AccessModeWrite), verifyAccept, verifyContentTypeJSON, basic.serveVerify) }) }, authenticate()) } |