]> source.dussan.org Git - gitea.git/commitdiff
Rewrite of the LFS server (#15523)
authorKN4CK3R <admin@oldschoolhack.me>
Sat, 5 Jun 2021 23:59:27 +0000 (01:59 +0200)
committerGitHub <noreply@github.com>
Sat, 5 Jun 2021 23:59:27 +0000 (02:59 +0300)
* Restructured code. Moved static checks out of loop.

* Restructured batch api. Add support for individual errors.

* Let router decide if LFS is enabled.

* Renamed methods.

* Return correct status from verify handler.

* Unified media type check in router.

* Changed error code according to spec.

* Moved checks into router.

* Removed invalid v1 api methods.

* Unified methods.

* Display better error messages.

* Added size parameter. Create meta object on upload.

* Use object error on invalid size.

* Skip upload if object exists.

* Moved methods.

* Suppress fields in response.

* Changed error on accept.

* Added tests.

* Use ErrorResponse object.

* Test against message property.

* Add support for the old invalid lfs client.

* Fixed the check because MinIO wraps the error.

* Use individual repositories.

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Lauris BH <lauris@nix.lv>
integrations/api_repo_lfs_locks_test.go
integrations/api_repo_lfs_test.go [new file with mode: 0644]
integrations/lfs_getobject_test.go
modules/lfs/shared.go
routers/routes/web.go
services/lfs/locks.go
services/lfs/server.go

index ffc239567dc91b227b29661307a8ffd76639c5a3..03549c11f4c209be38b21a9ae98d4bd11d2e6218 100644 (file)
@@ -11,6 +11,7 @@ import (
        "time"
 
        "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/lfs"
        "code.gitea.io/gitea/modules/setting"
        api "code.gitea.io/gitea/modules/structs"
 
@@ -40,7 +41,7 @@ func TestAPILFSLocksNotLogin(t *testing.T) {
        repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
 
        req := NewRequestf(t, "GET", "/%s/%s.git/info/lfs/locks", user.Name, repo.Name)
-       req.Header.Set("Accept", "application/vnd.git-lfs+json")
+       req.Header.Set("Accept", lfs.MediaType)
        resp := MakeRequest(t, req, http.StatusUnauthorized)
        var lfsLockError api.LFSLockError
        DecodeJSON(t, resp, &lfsLockError)
@@ -102,8 +103,8 @@ func TestAPILFSLocksLogged(t *testing.T) {
        for _, test := range tests {
                session := loginUser(t, test.user.Name)
                req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks", test.repo.FullName()), map[string]string{"path": test.path})
-               req.Header.Set("Accept", "application/vnd.git-lfs+json")
-               req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
+               req.Header.Set("Accept", lfs.MediaType)
+               req.Header.Set("Content-Type", lfs.MediaType)
                resp := session.MakeRequest(t, req, test.httpResult)
                if len(test.addTime) > 0 {
                        var lfsLock api.LFSLockResponse
@@ -119,7 +120,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
        for _, test := range resultsTests {
                session := loginUser(t, test.user.Name)
                req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
-               req.Header.Set("Accept", "application/vnd.git-lfs+json")
+               req.Header.Set("Accept", lfs.MediaType)
                resp := session.MakeRequest(t, req, http.StatusOK)
                var lfsLocks api.LFSLockList
                DecodeJSON(t, resp, &lfsLocks)
@@ -131,8 +132,8 @@ func TestAPILFSLocksLogged(t *testing.T) {
                }
 
                req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/verify", test.repo.FullName()), map[string]string{})
-               req.Header.Set("Accept", "application/vnd.git-lfs+json")
-               req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
+               req.Header.Set("Accept", lfs.MediaType)
+               req.Header.Set("Content-Type", lfs.MediaType)
                resp = session.MakeRequest(t, req, http.StatusOK)
                var lfsLocksVerify api.LFSLockListVerify
                DecodeJSON(t, resp, &lfsLocksVerify)
@@ -155,8 +156,8 @@ func TestAPILFSLocksLogged(t *testing.T) {
        for _, test := range deleteTests {
                session := loginUser(t, test.user.Name)
                req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/%s/unlock", test.repo.FullName(), test.lockID), map[string]string{})
-               req.Header.Set("Accept", "application/vnd.git-lfs+json")
-               req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
+               req.Header.Set("Accept", lfs.MediaType)
+               req.Header.Set("Content-Type", lfs.MediaType)
                resp := session.MakeRequest(t, req, http.StatusOK)
                var lfsLockRep api.LFSLockResponse
                DecodeJSON(t, resp, &lfsLockRep)
@@ -168,7 +169,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
        for _, test := range resultsTests {
                session := loginUser(t, test.user.Name)
                req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
-               req.Header.Set("Accept", "application/vnd.git-lfs+json")
+               req.Header.Set("Accept", lfs.MediaType)
                resp := session.MakeRequest(t, req, http.StatusOK)
                var lfsLocks api.LFSLockList
                DecodeJSON(t, resp, &lfsLocks)
diff --git a/integrations/api_repo_lfs_test.go b/integrations/api_repo_lfs_test.go
new file mode 100644 (file)
index 0000000..d0328fd
--- /dev/null
@@ -0,0 +1,466 @@
+// Copyright 2021 The Gitea 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 integrations
+
+import (
+       "bytes"
+       "net/http"
+       "path"
+       "strconv"
+       "strings"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/setting"
+
+       jsoniter "github.com/json-iterator/go"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestAPILFSNotStarted(t *testing.T) {
+       defer prepareTestEnv(t)()
+
+       setting.LFS.StartServer = false
+
+       user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+       repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+
+       req := NewRequestf(t, "POST", "/%s/%s.git/info/lfs/objects/batch", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusNotFound)
+       req = NewRequestf(t, "PUT", "/%s/%s.git/info/lfs/objects/oid/10", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusNotFound)
+       req = NewRequestf(t, "GET", "/%s/%s.git/info/lfs/objects/oid/name", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusNotFound)
+       req = NewRequestf(t, "GET", "/%s/%s.git/info/lfs/objects/oid", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusNotFound)
+       req = NewRequestf(t, "POST", "/%s/%s.git/info/lfs/verify", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestAPILFSMediaType(t *testing.T) {
+       defer prepareTestEnv(t)()
+
+       setting.LFS.StartServer = true
+
+       user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+       repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+
+       req := NewRequestf(t, "POST", "/%s/%s.git/info/lfs/objects/batch", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusUnsupportedMediaType)
+       req = NewRequestf(t, "POST", "/%s/%s.git/info/lfs/verify", user.Name, repo.Name)
+       MakeRequest(t, req, http.StatusUnsupportedMediaType)
+}
+
+func createLFSTestRepository(t *testing.T, name string) *models.Repository {
+       ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo")
+       t.Run("CreateRepo", doAPICreateRepository(ctx, false))
+
+       repo, err := models.GetRepositoryByOwnerAndName("user2", "lfs-"+name+"-repo")
+       assert.NoError(t, err)
+
+       return repo
+}
+
+func TestAPILFSBatch(t *testing.T) {
+       defer prepareTestEnv(t)()
+
+       setting.LFS.StartServer = true
+
+       repo := createLFSTestRepository(t, "batch")
+
+       content := []byte("dummy1")
+       oid := storeObjectInRepo(t, repo.ID, &content)
+       defer repo.RemoveLFSMetaObjectByOid(oid)
+
+       session := loginUser(t, "user2")
+
+       newRequest := func(t testing.TB, br *lfs.BatchRequest) *http.Request {
+               req := NewRequestWithJSON(t, "POST", "/user2/lfs-batch-repo.git/info/lfs/objects/batch", br)
+               req.Header.Set("Accept", lfs.MediaType)
+               req.Header.Set("Content-Type", lfs.MediaType)
+               return req
+       }
+       decodeResponse := func(t *testing.T, b *bytes.Buffer) *lfs.BatchResponse {
+               var br lfs.BatchResponse
+
+               json := jsoniter.ConfigCompatibleWithStandardLibrary
+               assert.NoError(t, json.Unmarshal(b.Bytes(), &br))
+               return &br
+       }
+
+       t.Run("InvalidJsonRequest", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, nil)
+
+               session.MakeRequest(t, req, http.StatusBadRequest)
+       })
+
+       t.Run("InvalidOperation", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, &lfs.BatchRequest{
+                       Operation: "dummy",
+               })
+
+               session.MakeRequest(t, req, http.StatusBadRequest)
+       })
+
+       t.Run("InvalidPointer", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, &lfs.BatchRequest{
+                       Operation: "download",
+                       Objects: []lfs.Pointer{
+                               {Oid: "dummy"},
+                               {Oid: oid, Size: -1},
+                       },
+               })
+
+               resp := session.MakeRequest(t, req, http.StatusOK)
+               br := decodeResponse(t, resp.Body)
+               assert.Len(t, br.Objects, 2)
+               assert.Equal(t, "dummy", br.Objects[0].Oid)
+               assert.Equal(t, oid, br.Objects[1].Oid)
+               assert.Equal(t, int64(0), br.Objects[0].Size)
+               assert.Equal(t, int64(-1), br.Objects[1].Size)
+               assert.NotNil(t, br.Objects[0].Error)
+               assert.NotNil(t, br.Objects[1].Error)
+               assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code)
+               assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[1].Error.Code)
+               assert.Equal(t, "Oid or size are invalid", br.Objects[0].Error.Message)
+               assert.Equal(t, "Oid or size are invalid", br.Objects[1].Error.Message)
+       })
+
+       t.Run("PointerSizeMissmatch", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, &lfs.BatchRequest{
+                       Operation: "download",
+                       Objects: []lfs.Pointer{
+                               {Oid: oid, Size: 1},
+                       },
+               })
+
+               resp := session.MakeRequest(t, req, http.StatusOK)
+               br := decodeResponse(t, resp.Body)
+               assert.Len(t, br.Objects, 1)
+               assert.NotNil(t, br.Objects[0].Error)
+               assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code)
+               assert.Equal(t, "Object "+oid+" is not 1 bytes", br.Objects[0].Error.Message)
+       })
+
+       t.Run("Download", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               t.Run("PointerNotInStore", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "download",
+                               Objects: []lfs.Pointer{
+                                       {Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab042", Size: 6},
+                               },
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.NotNil(t, br.Objects[0].Error)
+                       assert.Equal(t, http.StatusNotFound, br.Objects[0].Error.Code)
+               })
+
+               t.Run("MetaNotFound", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       p := lfs.Pointer{Oid: "05eeb4eb5be71f2dd291ca39157d6d9effd7d1ea19cbdc8a99411fe2a8f26a00", Size: 6}
+
+                       contentStore := lfs.NewContentStore()
+                       exist, err := contentStore.Exists(p)
+                       assert.NoError(t, err)
+                       assert.False(t, exist)
+                       err = contentStore.Put(p, bytes.NewReader([]byte("dummy0")))
+                       assert.NoError(t, err)
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "download",
+                               Objects:   []lfs.Pointer{p},
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.NotNil(t, br.Objects[0].Error)
+                       assert.Equal(t, http.StatusNotFound, br.Objects[0].Error.Code)
+               })
+
+               t.Run("Success", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "download",
+                               Objects: []lfs.Pointer{
+                                       {Oid: oid, Size: 6},
+                               },
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.Nil(t, br.Objects[0].Error)
+                       assert.Contains(t, br.Objects[0].Actions, "download")
+                       l := br.Objects[0].Actions["download"]
+                       assert.NotNil(t, l)
+                       assert.NotEmpty(t, l.Href)
+               })
+       })
+
+       t.Run("Upload", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               t.Run("FileTooBig", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       oldMaxFileSize := setting.LFS.MaxFileSize
+                       setting.LFS.MaxFileSize = 2
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "upload",
+                               Objects: []lfs.Pointer{
+                                       {Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab042", Size: 6},
+                               },
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.NotNil(t, br.Objects[0].Error)
+                       assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code)
+                       assert.Equal(t, "Size must be less than or equal to 2", br.Objects[0].Error.Message)
+
+                       setting.LFS.MaxFileSize = oldMaxFileSize
+               })
+
+               t.Run("AddMeta", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       p := lfs.Pointer{Oid: "05eeb4eb5be71f2dd291ca39157d6d9effd7d1ea19cbdc8a99411fe2a8f26a00", Size: 6}
+
+                       contentStore := lfs.NewContentStore()
+                       exist, err := contentStore.Exists(p)
+                       assert.NoError(t, err)
+                       assert.True(t, exist)
+
+                       meta, err := repo.GetLFSMetaObjectByOid(p.Oid)
+                       assert.Nil(t, meta)
+                       assert.Equal(t, models.ErrLFSObjectNotExist, err)
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "upload",
+                               Objects:   []lfs.Pointer{p},
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.Nil(t, br.Objects[0].Error)
+                       assert.Empty(t, br.Objects[0].Actions)
+
+                       meta, err = repo.GetLFSMetaObjectByOid(p.Oid)
+                       assert.NoError(t, err)
+                       assert.NotNil(t, meta)
+               })
+
+               t.Run("AlreadyExists", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "upload",
+                               Objects: []lfs.Pointer{
+                                       {Oid: oid, Size: 6},
+                               },
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.Nil(t, br.Objects[0].Error)
+                       assert.Empty(t, br.Objects[0].Actions)
+               })
+
+               t.Run("NewFile", func(t *testing.T) {
+                       defer PrintCurrentTest(t)()
+
+                       req := newRequest(t, &lfs.BatchRequest{
+                               Operation: "upload",
+                               Objects: []lfs.Pointer{
+                                       {Oid: "d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0153", Size: 1},
+                               },
+                       })
+
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       br := decodeResponse(t, resp.Body)
+                       assert.Len(t, br.Objects, 1)
+                       assert.Nil(t, br.Objects[0].Error)
+                       assert.Contains(t, br.Objects[0].Actions, "upload")
+                       ul := br.Objects[0].Actions["upload"]
+                       assert.NotNil(t, ul)
+                       assert.NotEmpty(t, ul.Href)
+                       assert.Contains(t, br.Objects[0].Actions, "verify")
+                       vl := br.Objects[0].Actions["verify"]
+                       assert.NotNil(t, vl)
+                       assert.NotEmpty(t, vl.Href)
+               })
+       })
+}
+
+func TestAPILFSUpload(t *testing.T) {
+       defer prepareTestEnv(t)()
+
+       setting.LFS.StartServer = true
+
+       repo := createLFSTestRepository(t, "upload")
+
+       content := []byte("dummy3")
+       oid := storeObjectInRepo(t, repo.ID, &content)
+       defer repo.RemoveLFSMetaObjectByOid(oid)
+
+       session := loginUser(t, "user2")
+
+       newRequest := func(t testing.TB, p lfs.Pointer, content string) *http.Request {
+               req := NewRequestWithBody(t, "PUT", path.Join("/user2/lfs-upload-repo.git/info/lfs/objects/", p.Oid, strconv.FormatInt(p.Size, 10)), strings.NewReader(content))
+               return req
+       }
+
+       t.Run("InvalidPointer", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, lfs.Pointer{Oid: "dummy"}, "")
+
+               session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+       })
+
+       t.Run("AlreadyExistsInStore", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               p := lfs.Pointer{Oid: "83de2e488b89a0aa1c97496b888120a28b0c1e15463a4adb8405578c540f36d4", Size: 6}
+
+               contentStore := lfs.NewContentStore()
+               exist, err := contentStore.Exists(p)
+               assert.NoError(t, err)
+               assert.False(t, exist)
+               err = contentStore.Put(p, bytes.NewReader([]byte("dummy5")))
+               assert.NoError(t, err)
+
+               meta, err := repo.GetLFSMetaObjectByOid(p.Oid)
+               assert.Nil(t, meta)
+               assert.Equal(t, models.ErrLFSObjectNotExist, err)
+
+               req := newRequest(t, p, "")
+
+               session.MakeRequest(t, req, http.StatusOK)
+
+               meta, err = repo.GetLFSMetaObjectByOid(p.Oid)
+               assert.NoError(t, err)
+               assert.NotNil(t, meta)
+       })
+
+       t.Run("MetaAlreadyExists", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, lfs.Pointer{Oid: oid, Size: 6}, "")
+
+               session.MakeRequest(t, req, http.StatusOK)
+       })
+
+       t.Run("HashMissmatch", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, lfs.Pointer{Oid: "2581dd7bbc1fe44726de4b7dd806a087a978b9c5aec0a60481259e34be09b06a", Size: 1}, "a")
+
+               session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+       })
+
+       t.Run("SizeMissmatch", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, lfs.Pointer{Oid: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", Size: 2}, "a")
+
+               session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+       })
+
+       t.Run("Success", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               p := lfs.Pointer{Oid: "6ccce4863b70f258d691f59609d31b4502e1ba5199942d3bc5d35d17a4ce771d", Size: 5}
+
+               req := newRequest(t, p, "gitea")
+
+               session.MakeRequest(t, req, http.StatusOK)
+
+               contentStore := lfs.NewContentStore()
+               exist, err := contentStore.Exists(p)
+               assert.NoError(t, err)
+               assert.True(t, exist)
+
+               meta, err := repo.GetLFSMetaObjectByOid(p.Oid)
+               assert.NoError(t, err)
+               assert.NotNil(t, meta)
+       })
+}
+
+func TestAPILFSVerify(t *testing.T) {
+       defer prepareTestEnv(t)()
+
+       setting.LFS.StartServer = true
+
+       repo := createLFSTestRepository(t, "verify")
+
+       content := []byte("dummy3")
+       oid := storeObjectInRepo(t, repo.ID, &content)
+       defer repo.RemoveLFSMetaObjectByOid(oid)
+
+       session := loginUser(t, "user2")
+
+       newRequest := func(t testing.TB, p *lfs.Pointer) *http.Request {
+               req := NewRequestWithJSON(t, "POST", "/user2/lfs-verify-repo.git/info/lfs/verify", p)
+               req.Header.Set("Accept", lfs.MediaType)
+               req.Header.Set("Content-Type", lfs.MediaType)
+               return req
+       }
+
+       t.Run("InvalidJsonRequest", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, nil)
+
+               session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+       })
+
+       t.Run("InvalidPointer", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, &lfs.Pointer{})
+
+               session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+       })
+
+       t.Run("PointerNotExisting", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, &lfs.Pointer{Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab042", Size: 6})
+
+               session.MakeRequest(t, req, http.StatusNotFound)
+       })
+
+       t.Run("Success", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               req := newRequest(t, &lfs.Pointer{Oid: oid, Size: 6})
+
+               session.MakeRequest(t, req, http.StatusOK)
+       })
+}
index 789c7572a77e544f6392ba8e89de03929e362c33..b7423a2dbe55fd6dc7c9a4098a2951882c74b402 100644 (file)
@@ -17,25 +17,16 @@ import (
        "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/routers/routes"
 
+       jsoniter "github.com/json-iterator/go"
        gzipp "github.com/klauspost/compress/gzip"
        "github.com/stretchr/testify/assert"
 )
 
-var lfsID = int64(20000)
-
 func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string {
        pointer, err := lfs.GeneratePointer(bytes.NewReader(*content))
        assert.NoError(t, err)
-       var lfsMetaObject *models.LFSMetaObject
 
-       if setting.Database.UsePostgreSQL {
-               lfsMetaObject = &models.LFSMetaObject{ID: lfsID, Pointer: pointer, RepositoryID: repositoryID}
-       } else {
-               lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repositoryID}
-       }
-
-       lfsID++
-       lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
+       _, err = models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: pointer, RepositoryID: repositoryID})
        assert.NoError(t, err)
        contentStore := lfs.NewContentStore()
        exist, err := contentStore.Exists(pointer)
@@ -210,7 +201,14 @@ func TestGetLFSRange(t *testing.T) {
                                "Range": []string{tt.in},
                        }
                        resp := storeAndGetLfs(t, &content, &h, tt.status)
-                       assert.Equal(t, tt.out, resp.Body.String())
+                       if tt.status == http.StatusPartialContent || tt.status == http.StatusOK {
+                               assert.Equal(t, tt.out, resp.Body.String())
+                       } else {
+                               var er lfs.ErrorResponse
+                               err := jsoniter.Unmarshal(resp.Body.Bytes(), &er)
+                               assert.NoError(t, err)
+                               assert.Equal(t, tt.out, er.Message)
+                       }
                })
        }
 }
index 70b76d7512d56eb3bab8277d74e853c6c7a9990d..9abbf85fbdc787546f11a3df529432e607af6338 100644 (file)
@@ -45,7 +45,7 @@ type BatchResponse struct {
 // ObjectResponse is object metadata as seen by clients of the LFS server.
 type ObjectResponse struct {
        Pointer
-       Actions map[string]*Link `json:"actions"`
+       Actions map[string]*Link `json:"actions,omitempty"`
        Error   *ObjectError     `json:"error,omitempty"`
 }
 
@@ -53,7 +53,7 @@ type ObjectResponse struct {
 type Link struct {
        Href      string            `json:"href"`
        Header    map[string]string `json:"header,omitempty"`
-       ExpiresAt time.Time         `json:"expires_at,omitempty"`
+       ExpiresAt *time.Time        `json:"expires_at,omitempty"`
 }
 
 // ObjectError defines the JSON structure returned to the client in case of an error
@@ -67,3 +67,10 @@ type PointerBlob struct {
        Hash string
        Pointer
 }
+
+// ErrorResponse describes the error to the client.
+type ErrorResponse struct {
+       Message          string
+       DocumentationURL string `json:"documentation_url,omitempty"`
+       RequestID        string `json:"request_id,omitempty"`
+}
index 6d91eb1b3c80edee5f7925ede8cd49e610571519..fbc41d547d16316497ef6b42e6cedc68724d795b 100644 (file)
@@ -286,6 +286,13 @@ func RegisterRoutes(m *web.Route) {
                }
        }
 
+       lfsServerEnabled := func(ctx *context.Context) {
+               if !setting.LFS.StartServer {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+       }
+
        // FIXME: not all routes need go through same middleware.
        // Especially some AJAX requests, we can reduce middleware number to improve performance.
        // Routers.
@@ -1042,21 +1049,21 @@ func RegisterRoutes(m *web.Route) {
 
                m.Group("/{reponame}", func() {
                        m.Group("/info/lfs", func() {
-                               m.Post("/objects/batch", lfs.BatchHandler)
-                               m.Get("/objects/{oid}/{filename}", lfs.ObjectOidHandler)
-                               m.Any("/objects/{oid}", lfs.ObjectOidHandler)
-                               m.Post("/objects", lfs.PostHandler)
-                               m.Post("/verify", lfs.VerifyHandler)
+                               m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
+                               m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
+                               m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
+                               m.Get("/objects/{oid}", lfs.DownloadHandler)
+                               m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
                                m.Group("/locks", func() {
                                        m.Get("/", lfs.GetListLockHandler)
                                        m.Post("/", lfs.PostLockHandler)
                                        m.Post("/verify", lfs.VerifyLockHandler)
                                        m.Post("/{lid}/unlock", lfs.UnLockHandler)
-                               })
+                               }, lfs.CheckAcceptMediaType)
                                m.Any("/*", func(ctx *context.Context) {
                                        ctx.NotFound("", nil)
                                })
-                       }, ignSignInAndCsrf)
+                       }, ignSignInAndCsrf, lfsServerEnabled)
 
                        m.Group("", func() {
                                m.Post("/git-upload-pack", repo.ServiceUploadPack)
index ad204c46e2b2646719bd992d276e505fb44e0c5d..20ba12e65bb20187b3ff9578d7b0cafabef0770d 100644 (file)
@@ -19,21 +19,6 @@ import (
        jsoniter "github.com/json-iterator/go"
 )
 
-//checkIsValidRequest check if it a valid request in case of bad request it write the response to ctx.
-func checkIsValidRequest(ctx *context.Context) bool {
-       if !setting.LFS.StartServer {
-               log.Debug("Attempt to access LFS server but LFS server is disabled")
-               writeStatus(ctx, http.StatusNotFound)
-               return false
-       }
-       if !MetaMatcher(ctx.Req) {
-               log.Info("Attempt access LOCKs without accepting the correct media type: %s", lfs_module.MediaType)
-               writeStatus(ctx, http.StatusBadRequest)
-               return false
-       }
-       return true
-}
-
 func handleLockListOut(ctx *context.Context, repo *models.Repository, lock *models.LFSLock, err error) {
        if err != nil {
                if models.IsErrLFSLockNotExist(err) {
@@ -60,12 +45,7 @@ func handleLockListOut(ctx *context.Context, repo *models.Repository, lock *mode
 
 // GetListLockHandler list locks
 func GetListLockHandler(ctx *context.Context) {
-       if !checkIsValidRequest(ctx) {
-               // Status is written in checkIsValidRequest
-               return
-       }
-
-       rv, _ := unpack(ctx)
+       rv := getRequestContext(ctx)
 
        repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)
        if err != nil {
@@ -150,11 +130,6 @@ func GetListLockHandler(ctx *context.Context) {
 
 // PostLockHandler create lock
 func PostLockHandler(ctx *context.Context) {
-       if !checkIsValidRequest(ctx) {
-               // Status is written in checkIsValidRequest
-               return
-       }
-
        userName := ctx.Params("username")
        repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
        authorization := ctx.Req.Header.Get("Authorization")
@@ -223,11 +198,6 @@ func PostLockHandler(ctx *context.Context) {
 
 // VerifyLockHandler list locks for verification
 func VerifyLockHandler(ctx *context.Context) {
-       if !checkIsValidRequest(ctx) {
-               // Status is written in checkIsValidRequest
-               return
-       }
-
        userName := ctx.Params("username")
        repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
        authorization := ctx.Req.Header.Get("Authorization")
@@ -294,11 +264,6 @@ func VerifyLockHandler(ctx *context.Context) {
 
 // UnLockHandler delete locks
 func UnLockHandler(ctx *context.Context) {
-       if !checkIsValidRequest(ctx) {
-               // Status is written in checkIsValidRequest
-               return
-       }
-
        userName := ctx.Params("username")
        repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
        authorization := ctx.Req.Header.Get("Authorization")
index ee7d3bc79a3ee2a9e90f6118884176645091a1b7..9954534b5e9aebcf533c181948a9ac68079dfcce 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2020 The Gitea Authors. All rights reserved.
+// Copyright 2021 The Gitea Authors. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
@@ -6,6 +6,7 @@ package lfs
 
 import (
        "encoding/base64"
+       "errors"
        "fmt"
        "io"
        "net/http"
@@ -39,95 +40,51 @@ type Claims struct {
        jwt.StandardClaims
 }
 
-// ObjectLink builds a URL linking to the object.
-func (rc *requestContext) ObjectLink(oid string) string {
-       return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/objects", oid)
+// DownloadLink builds a URL to download the object.
+func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
+       return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/objects", p.Oid)
 }
 
-// VerifyLink builds a URL for verifying the object.
-func (rc *requestContext) VerifyLink() string {
-       return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/verify")
+// UploadLink builds a URL to upload the object.
+func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
+       return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/objects", p.Oid, strconv.FormatInt(p.Size, 10))
 }
 
-var oidRegExp = regexp.MustCompile(`^[A-Fa-f0-9]+$`)
-
-func isOidValid(oid string) bool {
-       return oidRegExp.MatchString(oid)
+// VerifyLink builds a URL for verifying the object.
+func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
+       return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/verify")
 }
 
-// ObjectOidHandler is the main request routing entry point into LFS server functions
-func ObjectOidHandler(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               log.Debug("Attempt to access LFS server but LFS server is disabled")
-               writeStatus(ctx, 404)
-               return
-       }
-
-       if ctx.Req.Method == "GET" || ctx.Req.Method == "HEAD" {
-               if MetaMatcher(ctx.Req) {
-                       getMetaHandler(ctx)
-                       return
-               }
+// CheckAcceptMediaType checks if the client accepts the LFS media type.
+func CheckAcceptMediaType(ctx *context.Context) {
+       mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";")
 
-               getContentHandler(ctx)
-               return
-       } else if ctx.Req.Method == "PUT" {
-               PutHandler(ctx)
+       if mediaParts[0] != lfs_module.MediaType {
+               log.Trace("Calling a LFS method without accepting the correct media type: %s", lfs_module.MediaType)
+               writeStatus(ctx, http.StatusUnsupportedMediaType)
                return
        }
-
-       log.Warn("Unhandled LFS method: %s for %s/%s OID[%s]", ctx.Req.Method, ctx.Params("username"), ctx.Params("reponame"), ctx.Params("oid"))
-       writeStatus(ctx, 404)
 }
 
-func getAuthenticatedRepoAndMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) (*models.LFSMetaObject, *models.Repository) {
-       if !isOidValid(p.Oid) {
-               log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
-               writeStatus(ctx, 404)
-               return nil, nil
-       }
-
-       repository, err := models.GetRepositoryByOwnerAndName(rc.User, rc.Repo)
-       if err != nil {
-               log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
-               writeStatus(ctx, 404)
-               return nil, nil
-       }
-
-       if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
-               requireAuth(ctx)
-               return nil, nil
-       }
-
-       meta, err := repository.GetLFSMetaObjectByOid(p.Oid)
-       if err != nil {
-               log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)
-               writeStatus(ctx, 404)
-               return nil, nil
-       }
-
-       return meta, repository
-}
-
-// getContentHandler gets the content from the content store
-func getContentHandler(ctx *context.Context) {
-       rc, p := unpack(ctx)
+// DownloadHandler gets the content from the content store
+func DownloadHandler(ctx *context.Context) {
+       rc := getRequestContext(ctx)
+       p := lfs_module.Pointer{Oid: ctx.Params("oid")}
 
-       meta, _ := getAuthenticatedRepoAndMeta(ctx, rc, p, false)
+       meta := getAuthenticatedMeta(ctx, rc, p, false)
        if meta == nil {
-               // Status already written in getAuthenticatedRepoAndMeta
                return
        }
 
        // Support resume download using Range header
        var fromByte, toByte int64
        toByte = meta.Size - 1
-       statusCode := 200
+       statusCode := http.StatusOK
        if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
                regex := regexp.MustCompile(`bytes=(\d+)\-(\d*).*`)
                match := regex.FindStringSubmatch(rangeHdr)
                if len(match) > 1 {
-                       statusCode = 206
+                       statusCode = http.StatusPartialContent
                        fromByte, _ = strconv.ParseInt(match[1], 10, 32)
 
                        if fromByte >= meta.Size {
@@ -150,7 +107,6 @@ func getContentHandler(ctx *context.Context) {
        contentStore := lfs_module.NewContentStore()
        content, err := contentStore.Get(meta.Pointer)
        if err != nil {
-               // Errors are logged in contentStore.Get
                writeStatus(ctx, http.StatusNotFound)
                return
        }
@@ -183,380 +139,300 @@ func getContentHandler(ctx *context.Context) {
        if written, err := io.CopyN(ctx.Resp, content, contentLength); err != nil {
                log.Error("Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v", meta.Oid, written, err)
        }
-       logRequest(ctx.Req, statusCode)
-}
-
-// getMetaHandler retrieves metadata about the object
-func getMetaHandler(ctx *context.Context) {
-       rc, p := unpack(ctx)
-
-       meta, _ := getAuthenticatedRepoAndMeta(ctx, rc, p, false)
-       if meta == nil {
-               // Status already written in getAuthenticatedRepoAndMeta
-               return
-       }
-
-       ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
-
-       if ctx.Req.Method == "GET" {
-               json := jsoniter.ConfigCompatibleWithStandardLibrary
-               enc := json.NewEncoder(ctx.Resp)
-               if err := enc.Encode(represent(rc, meta.Pointer, true, false)); err != nil {
-                       log.Error("Failed to encode representation as json. Error: %v", err)
-               }
-       }
-
-       logRequest(ctx.Req, 200)
 }
 
-// PostHandler instructs the client how to upload data
-func PostHandler(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               log.Debug("Attempt to access LFS server but LFS server is disabled")
-               writeStatus(ctx, 404)
-               return
-       }
-
-       if !MetaMatcher(ctx.Req) {
-               log.Info("Attempt to POST without accepting the correct media type: %s", lfs_module.MediaType)
-               writeStatus(ctx, 400)
-               return
-       }
-
-       rc, p := unpack(ctx)
-
-       repository, err := models.GetRepositoryByOwnerAndName(rc.User, rc.Repo)
-       if err != nil {
-               log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
-               writeStatus(ctx, 404)
-               return
-       }
-
-       if !authenticate(ctx, repository, rc.Authorization, false, true) {
-               requireAuth(ctx)
+// BatchHandler provides the batch api
+func BatchHandler(ctx *context.Context) {
+       var br lfs_module.BatchRequest
+       if err := decodeJSON(ctx.Req, &br); err != nil {
+               log.Trace("Unable to decode BATCH request vars: Error: %v", err)
+               writeStatus(ctx, http.StatusBadRequest)
                return
        }
 
-       if !isOidValid(p.Oid) {
-               log.Info("Invalid LFS OID[%s] attempt to POST in %s/%s", p.Oid, rc.User, rc.Repo)
-               writeStatus(ctx, 404)
+       var isUpload bool
+       if br.Operation == "upload" {
+               isUpload = true
+       } else if br.Operation == "download" {
+               isUpload = false
+       } else {
+               log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)
+               writeStatus(ctx, http.StatusBadRequest)
                return
        }
 
-       if setting.LFS.MaxFileSize > 0 && p.Size > setting.LFS.MaxFileSize {
-               log.Info("Denied LFS OID[%s] upload of size %d to %s/%s because of LFS_MAX_FILE_SIZE=%d", p.Oid, p.Size, rc.User, rc.Repo, setting.LFS.MaxFileSize)
-               writeStatus(ctx, 413)
-               return
-       }
+       rc := getRequestContext(ctx)
 
-       meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
-       if err != nil {
-               log.Error("Unable to write LFS OID[%s] size %d meta object in %v/%v to database. Error: %v", p.Oid, p.Size, rc.User, rc.Repo, err)
-               writeStatus(ctx, 404)
+       repository := getAuthenticatedRepository(ctx, rc, isUpload)
+       if repository == nil {
                return
        }
 
-       ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
-
-       sentStatus := 202
        contentStore := lfs_module.NewContentStore()
-       exist, err := contentStore.Exists(p)
-       if err != nil {
-               log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", p.Oid, rc.User, rc.Repo, err)
-               writeStatus(ctx, 500)
-               return
-       }
-       if meta.Existing && exist {
-               sentStatus = 200
-       }
-       ctx.Resp.WriteHeader(sentStatus)
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       enc := json.NewEncoder(ctx.Resp)
-       if err := enc.Encode(represent(rc, meta.Pointer, meta.Existing, true)); err != nil {
-               log.Error("Failed to encode representation as json. Error: %v", err)
-       }
-       logRequest(ctx.Req, sentStatus)
-}
-
-// BatchHandler provides the batch api
-func BatchHandler(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               log.Debug("Attempt to access LFS server but LFS server is disabled")
-               writeStatus(ctx, 404)
-               return
-       }
-
-       if !MetaMatcher(ctx.Req) {
-               log.Info("Attempt to BATCH without accepting the correct media type: %s", lfs_module.MediaType)
-               writeStatus(ctx, 400)
-               return
-       }
-
-       bv := unpackbatch(ctx)
-
-       reqCtx := &requestContext{
-               User:          ctx.Params("username"),
-               Repo:          strings.TrimSuffix(ctx.Params("reponame"), ".git"),
-               Authorization: ctx.Req.Header.Get("Authorization"),
-       }
 
        var responseObjects []*lfs_module.ObjectResponse
 
-       // Create a response object
-       for _, object := range bv.Objects {
-               if !isOidValid(object.Oid) {
-                       log.Info("Invalid LFS OID[%s] attempt to BATCH in %s/%s", object.Oid, reqCtx.User, reqCtx.Repo)
+       for _, p := range br.Objects {
+               if !p.IsValid() {
+                       responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
+                               Code:    http.StatusUnprocessableEntity,
+                               Message: "Oid or size are invalid",
+                       }))
                        continue
                }
 
-               repository, err := models.GetRepositoryByOwnerAndName(reqCtx.User, reqCtx.Repo)
+               exists, err := contentStore.Exists(p)
                if err != nil {
-                       log.Error("Unable to get repository: %s/%s Error: %v", reqCtx.User, reqCtx.Repo, err)
-                       writeStatus(ctx, 404)
+                       log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, rc.User, rc.Repo, err)
+                       writeStatus(ctx, http.StatusInternalServerError)
                        return
                }
 
-               requireWrite := false
-               if bv.Operation == "upload" {
-                       requireWrite = true
-               }
-
-               if !authenticate(ctx, repository, reqCtx.Authorization, false, requireWrite) {
-                       requireAuth(ctx)
+               meta, err := repository.GetLFSMetaObjectByOid(p.Oid)
+               if err != nil && err != models.ErrLFSObjectNotExist {
+                       log.Error("Unable to get LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
+                       writeStatus(ctx, http.StatusInternalServerError)
                        return
                }
 
-               contentStore := lfs_module.NewContentStore()
-
-               meta, err := repository.GetLFSMetaObjectByOid(object.Oid)
-               if err == nil { // Object is found and exists
-                       exist, err := contentStore.Exists(meta.Pointer)
-                       if err != nil {
-                               log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", object.Oid, reqCtx.User, reqCtx.Repo, err)
-                               writeStatus(ctx, 500)
-                               return
-                       }
-                       if exist {
-                               responseObjects = append(responseObjects, represent(reqCtx, meta.Pointer, true, false))
-                               continue
-                       }
+               if meta != nil && p.Size != meta.Size {
+                       responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
+                               Code:    http.StatusUnprocessableEntity,
+                               Message: fmt.Sprintf("Object %s is not %d bytes", p.Oid, p.Size),
+                       }))
+                       continue
                }
 
-               if requireWrite && setting.LFS.MaxFileSize > 0 && object.Size > setting.LFS.MaxFileSize {
-                       log.Info("Denied LFS OID[%s] upload of size %d to %s/%s because of LFS_MAX_FILE_SIZE=%d", object.Oid, object.Size, reqCtx.User, reqCtx.Repo, setting.LFS.MaxFileSize)
-                       writeStatus(ctx, 413)
-                       return
-               }
+               var responseObject *lfs_module.ObjectResponse
+               if isUpload {
+                       var err *lfs_module.ObjectError
+                       if !exists && setting.LFS.MaxFileSize > 0 && p.Size > setting.LFS.MaxFileSize {
+                               err = &lfs_module.ObjectError{
+                                       Code:    http.StatusUnprocessableEntity,
+                                       Message: fmt.Sprintf("Size must be less than or equal to %d", setting.LFS.MaxFileSize),
+                               }
+                       }
 
-               // Object is not found
-               meta, err = models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: object, RepositoryID: repository.ID})
-               if err == nil {
-                       exist, err := contentStore.Exists(meta.Pointer)
-                       if err != nil {
-                               log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", object.Oid, reqCtx.User, reqCtx.Repo, err)
-                               writeStatus(ctx, 500)
-                               return
+                       if exists {
+                               if meta == nil {
+                                       _, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
+                                       if err != nil {
+                                               log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
+                                               writeStatus(ctx, http.StatusInternalServerError)
+                                               return
+                                       }
+                               }
                        }
-                       responseObjects = append(responseObjects, represent(reqCtx, meta.Pointer, meta.Existing, !exist))
+
+                       responseObject = buildObjectResponse(rc, p, false, !exists, err)
                } else {
-                       log.Error("Unable to write LFS OID[%s] size %d meta object in %v/%v to database. Error: %v", object.Oid, object.Size, reqCtx.User, reqCtx.Repo, err)
+                       var err *lfs_module.ObjectError
+                       if !exists || meta == nil {
+                               err = &lfs_module.ObjectError{
+                                       Code:    http.StatusNotFound,
+                                       Message: http.StatusText(http.StatusNotFound),
+                               }
+                       }
+
+                       responseObject = buildObjectResponse(rc, p, true, false, err)
                }
+               responseObjects = append(responseObjects, responseObject)
        }
 
-       ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
-
        respobj := &lfs_module.BatchResponse{Objects: responseObjects}
 
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       enc := json.NewEncoder(ctx.Resp)
+       ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
+
+       enc := jsoniter.NewEncoder(ctx.Resp)
        if err := enc.Encode(respobj); err != nil {
                log.Error("Failed to encode representation as json. Error: %v", err)
        }
-       logRequest(ctx.Req, 200)
 }
 
-// PutHandler receives data from the client and puts it into the content store
-func PutHandler(ctx *context.Context) {
-       rc, p := unpack(ctx)
+// UploadHandler receives data from the client and puts it into the content store
+func UploadHandler(ctx *context.Context) {
+       rc := getRequestContext(ctx)
 
-       meta, repository := getAuthenticatedRepoAndMeta(ctx, rc, p, true)
-       if meta == nil {
-               // Status already written in getAuthenticatedRepoAndMeta
+       p := lfs_module.Pointer{Oid: ctx.Params("oid")}
+       var err error
+       if p.Size, err = strconv.ParseInt(ctx.Params("size"), 10, 64); err != nil {
+               writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
+       }
+
+       if !p.IsValid() {
+               log.Trace("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
+               writeStatus(ctx, http.StatusUnprocessableEntity)
+               return
+       }
+
+       repository := getAuthenticatedRepository(ctx, rc, true)
+       if repository == nil {
+               return
+       }
+
+       meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
+       if err != nil {
+               log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
+               writeStatus(ctx, http.StatusInternalServerError)
                return
        }
 
        contentStore := lfs_module.NewContentStore()
+
+       exists, err := contentStore.Exists(p)
+       if err != nil {
+               log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, err)
+               writeStatus(ctx, http.StatusInternalServerError)
+               return
+       }
+       if meta.Existing || exists {
+               ctx.Resp.WriteHeader(http.StatusOK)
+               return
+       }
+
        defer ctx.Req.Body.Close()
        if err := contentStore.Put(meta.Pointer, ctx.Req.Body); err != nil {
-               // Put will log the error itself
-               ctx.Resp.WriteHeader(500)
-               if err == lfs_module.ErrSizeMismatch || err == lfs_module.ErrHashMismatch {
-                       fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err)
+               if errors.Is(err, lfs_module.ErrSizeMismatch) || errors.Is(err, lfs_module.ErrHashMismatch) {
+                       writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
                } else {
-                       fmt.Fprintf(ctx.Resp, `{"message":"Internal Server Error"}`)
+                       writeStatus(ctx, http.StatusInternalServerError)
                }
                if _, err = repository.RemoveLFSMetaObjectByOid(p.Oid); err != nil {
-                       log.Error("Whilst removing metaobject for LFS OID[%s] due to preceding error there was another Error: %v", p.Oid, err)
+                       log.Error("Error whilst removing metaobject for LFS OID[%s]: %v", p.Oid, err)
                }
                return
        }
 
-       logRequest(ctx.Req, 200)
+       writeStatus(ctx, http.StatusOK)
 }
 
 // VerifyHandler verify oid and its size from the content store
 func VerifyHandler(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               log.Debug("Attempt to access LFS server but LFS server is disabled")
-               writeStatus(ctx, 404)
-               return
-       }
-
-       if !MetaMatcher(ctx.Req) {
-               log.Info("Attempt to VERIFY without accepting the correct media type: %s", lfs_module.MediaType)
-               writeStatus(ctx, 400)
+       var p lfs_module.Pointer
+       if err := decodeJSON(ctx.Req, &p); err != nil {
+               writeStatus(ctx, http.StatusUnprocessableEntity)
                return
        }
 
-       rc, p := unpack(ctx)
+       rc := getRequestContext(ctx)
 
-       meta, _ := getAuthenticatedRepoAndMeta(ctx, rc, p, true)
+       meta := getAuthenticatedMeta(ctx, rc, p, true)
        if meta == nil {
-               // Status already written in getAuthenticatedRepoAndMeta
                return
        }
 
        contentStore := lfs_module.NewContentStore()
        ok, err := contentStore.Verify(meta.Pointer)
+
+       status := http.StatusOK
        if err != nil {
-               // Error will be logged in Verify
-               ctx.Resp.WriteHeader(500)
-               fmt.Fprintf(ctx.Resp, `{"message":"Internal Server Error"}`)
-               return
-       }
-       if !ok {
-               writeStatus(ctx, 422)
-               return
+               status = http.StatusInternalServerError
+       } else if !ok {
+               status = http.StatusNotFound
        }
+       writeStatus(ctx, status)
+}
+
+func decodeJSON(req *http.Request, v interface{}) error {
+       defer req.Body.Close()
 
-       logRequest(ctx.Req, 200)
+       dec := jsoniter.NewDecoder(req.Body)
+       return dec.Decode(v)
 }
 
-// represent takes a requestContext and Meta and turns it into a ObjectResponse suitable
-// for json encoding
-func represent(rc *requestContext, pointer lfs_module.Pointer, download, upload bool) *lfs_module.ObjectResponse {
-       rep := &lfs_module.ObjectResponse{
-               Pointer: pointer,
-               Actions: make(map[string]*lfs_module.Link),
+func getRequestContext(ctx *context.Context) *requestContext {
+       return &requestContext{
+               User:          ctx.Params("username"),
+               Repo:          strings.TrimSuffix(ctx.Params("reponame"), ".git"),
+               Authorization: ctx.Req.Header.Get("Authorization"),
        }
+}
 
-       header := make(map[string]string)
-
-       if rc.Authorization == "" {
-               //https://github.com/github/git-lfs/issues/1088
-               header["Authorization"] = "Authorization: Basic dummy"
-       } else {
-               header["Authorization"] = rc.Authorization
+func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *models.LFSMetaObject {
+       if !p.IsValid() {
+               log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
+               writeStatusMessage(ctx, http.StatusUnprocessableEntity, "Oid or size are invalid")
+               return nil
        }
 
-       if download {
-               rep.Actions["download"] = &lfs_module.Link{Href: rc.ObjectLink(pointer.Oid), Header: header}
+       repository := getAuthenticatedRepository(ctx, rc, requireWrite)
+       if repository == nil {
+               return nil
        }
 
-       if upload {
-               rep.Actions["upload"] = &lfs_module.Link{Href: rc.ObjectLink(pointer.Oid), Header: header}
+       meta, err := repository.GetLFSMetaObjectByOid(p.Oid)
+       if err != nil {
+               log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)
+               writeStatus(ctx, http.StatusNotFound)
+               return nil
        }
 
-       if upload && !download {
-               // Force client side verify action while gitea lacks proper server side verification
-               verifyHeader := make(map[string]string)
-               for k, v := range header {
-                       verifyHeader[k] = v
-               }
+       return meta
+}
 
-               // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
-               verifyHeader["Accept"] = lfs_module.MediaType
+func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *models.Repository {
+       repository, err := models.GetRepositoryByOwnerAndName(rc.User, rc.Repo)
+       if err != nil {
+               log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
+               writeStatus(ctx, http.StatusNotFound)
+               return nil
+       }
 
-               rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(), Header: verifyHeader}
+       if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
+               requireAuth(ctx)
+               return nil
        }
 
-       return rep
+       return repository
 }
 
-// MetaMatcher provides a mux.MatcherFunc that only allows requests that contain
-// an Accept header with the lfs_module.MediaType
-func MetaMatcher(r *http.Request) bool {
-       mediaParts := strings.Split(r.Header.Get("Accept"), ";")
-       mt := mediaParts[0]
-       return mt == lfs_module.MediaType
-}
+func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, download, upload bool, err *lfs_module.ObjectError) *lfs_module.ObjectResponse {
+       rep := &lfs_module.ObjectResponse{Pointer: pointer}
+       if err != nil {
+               rep.Error = err
+       } else {
+               rep.Actions = make(map[string]*lfs_module.Link)
 
-func unpack(ctx *context.Context) (*requestContext, lfs_module.Pointer) {
-       r := ctx.Req
-       rc := &requestContext{
-               User:          ctx.Params("username"),
-               Repo:          strings.TrimSuffix(ctx.Params("reponame"), ".git"),
-               Authorization: r.Header.Get("Authorization"),
-       }
-       p := lfs_module.Pointer{Oid: ctx.Params("oid")}
+               header := make(map[string]string)
 
-       if r.Method == "POST" { // Maybe also check if +json
-               var p2 lfs_module.Pointer
-               bodyReader := r.Body
-               defer bodyReader.Close()
-               json := jsoniter.ConfigCompatibleWithStandardLibrary
-               dec := json.NewDecoder(bodyReader)
-               err := dec.Decode(&p2)
-               if err != nil {
-                       // The error is logged as a WARN here because this may represent misbehaviour rather than a true error
-                       log.Warn("Unable to decode POST request vars for LFS OID[%s] in %s/%s: Error: %v", p.Oid, rc.User, rc.Repo, err)
-                       return rc, p
+               if len(rc.Authorization) > 0 {
+                       header["Authorization"] = rc.Authorization
                }
 
-               p.Oid = p2.Oid
-               p.Size = p2.Size
-       }
-
-       return rc, p
-}
+               if download {
+                       rep.Actions["download"] = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header}
+               }
+               if upload {
+                       rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header}
 
-// TODO cheap hack, unify with unpack
-func unpackbatch(ctx *context.Context) *lfs_module.BatchRequest {
+                       verifyHeader := make(map[string]string)
+                       for key, value := range header {
+                               verifyHeader[key] = value
+                       }
 
-       r := ctx.Req
-       var bv lfs_module.BatchRequest
+                       // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
+                       verifyHeader["Accept"] = lfs_module.MediaType
 
-       bodyReader := r.Body
-       defer bodyReader.Close()
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       dec := json.NewDecoder(bodyReader)
-       err := dec.Decode(&bv)
-       if err != nil {
-               // The error is logged as a WARN here because this may represent misbehaviour rather than a true error
-               log.Warn("Unable to decode BATCH request vars in %s/%s: Error: %v", ctx.Params("username"), strings.TrimSuffix(ctx.Params("reponame"), ".git"), err)
-               return &bv
+                       rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader}
+               }
        }
-
-       return &bv
+       return rep
 }
 
 func writeStatus(ctx *context.Context, status int) {
-       message := http.StatusText(status)
-
-       mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";")
-       mt := mediaParts[0]
-       if strings.HasSuffix(mt, "+json") {
-               message = `{"message":"` + message + `"}`
-       }
+       writeStatusMessage(ctx, status, http.StatusText(status))
+}
 
+func writeStatusMessage(ctx *context.Context, status int, message string) {
+       ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
        ctx.Resp.WriteHeader(status)
-       fmt.Fprint(ctx.Resp, message)
-       logRequest(ctx.Req, status)
-}
 
-func logRequest(r *http.Request, status int) {
-       log.Debug("LFS request - Method: %s, URL: %s, Status %d", r.Method, r.URL, status)
+       er := lfs_module.ErrorResponse{Message: message}
+
+       enc := jsoniter.NewEncoder(ctx.Resp)
+       if err := enc.Encode(er); err != nil {
+               log.Error("Failed to encode error response as json. Error: %v", err)
+       }
 }
 
 // authenticate uses the authorization string to determine whether
@@ -645,5 +521,5 @@ func parseToken(authorization string, target *models.Repository, mode models.Acc
 
 func requireAuth(ctx *context.Context) {
        ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
-       writeStatus(ctx, 401)
+       writeStatus(ctx, http.StatusUnauthorized)
 }