mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-10 05:24:18 +09:00
fix(lfs): failed upload deletes a concurrent upload's meta object (#38693)
UploadHandler creates the LFS meta object only as the last step of uploadOrVerify, after the content is already in the store, so a request that errors has never created a row of its own. The removal on the error path was a real compensating action when it was added in #14726, where the meta object was created before contentStore.Put, but #16865 moved creation after the Put and left the removal behind. Since then it can only ever delete a row created by a different request: a stalled git-lfs PUT that fails after its own retry has already succeeded wipes the winner's meta object, leaving the content in the store unreachable and eligible for orphan cleanup. Drop the removal and add a regression test asserting that a failing upload keeps a pre-existing meta object. Fixes: #38424 Assisted-by: Claude Code:claude-opus-5 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -5,9 +5,8 @@ package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -59,6 +58,7 @@ func TestAPILFSMediaType(t *testing.T) {
|
||||
}
|
||||
|
||||
func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
|
||||
t.Helper()
|
||||
ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
|
||||
|
||||
@@ -281,7 +281,7 @@ func TestAPILFSBatch(t *testing.T) {
|
||||
assert.Equal(t, git_model.ErrLFSObjectNotExist, err)
|
||||
|
||||
// Cleanup
|
||||
err = contentStore.Delete(p.RelativePath())
|
||||
err = contentStore.ObjectStorage.Delete(p.RelativePath())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -334,13 +334,11 @@ func TestAPILFSUpload(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.LFS.StartServer, true)()
|
||||
|
||||
repo := createLFSTestRepository(t, "lfs-upload-repo")
|
||||
oid := storeObjectInRepo(t, repo.ID, "dummy3")
|
||||
defer git_model.RemoveLFSMetaObjectByOid(t.Context(), repo.ID, oid)
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
newRequest := func(t testing.TB, p lfs.Pointer, content string) *RequestWrapper {
|
||||
return NewRequestWithBody(t, "PUT", path.Join("/user2/lfs-upload-repo.git/info/lfs/objects/", p.Oid, strconv.FormatInt(p.Size, 10)), strings.NewReader(content))
|
||||
reqUrl := fmt.Sprintf("/user2/lfs-upload-repo.git/info/lfs/objects/%s/%d", p.Oid, p.Size)
|
||||
return NewRequestWithBody(t, "PUT", reqUrl, strings.NewReader(content))
|
||||
}
|
||||
|
||||
t.Run("InvalidPointer", func(t *testing.T) {
|
||||
@@ -382,15 +380,14 @@ func TestAPILFSUpload(t *testing.T) {
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
err = contentStore.Delete(p.RelativePath())
|
||||
err = contentStore.ObjectStorage.Delete(p.RelativePath())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("MetaAlreadyExists", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
oid := storeObjectInRepo(t, repo.ID, "123456")
|
||||
req := newRequest(t, lfs.Pointer{Oid: oid, Size: 6}, "")
|
||||
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
|
||||
@@ -410,6 +407,24 @@ func TestAPILFSUpload(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
})
|
||||
|
||||
t.Run("ConcurrentFailureKeepsExistingMeta", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
pointer, err := lfs.GeneratePointer(strings.NewReader("any-content"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// mock a record in database: it should not happen in real world (no existing file in the store)
|
||||
// !!for testing purpose only!! to verify a failed upload should not remove a valid record,
|
||||
_, err = git_model.NewLFSMetaObject(t.Context(), repo.ID, pointer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// make an invalid request, the existing lfs record should not be removed
|
||||
req := newRequest(t, lfs.Pointer{Oid: pointer.Oid, Size: 1}, "invalid content")
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
meta, err := git_model.GetLFSMetaObjectByOid(t.Context(), repo.ID, pointer.Oid)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, meta)
|
||||
})
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
|
||||
@@ -26,18 +26,14 @@ import (
|
||||
)
|
||||
|
||||
func storeObjectInRepo(t *testing.T, repositoryID int64, content string) string {
|
||||
t.Helper()
|
||||
pointer, err := lfs.GeneratePointer(strings.NewReader(content))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = git_model.NewLFSMetaObject(t.Context(), repositoryID, pointer)
|
||||
assert.NoError(t, err)
|
||||
contentStore := lfs.NewContentStore()
|
||||
exist, err := contentStore.Exists(pointer)
|
||||
err = lfs.NewContentStore().Put(pointer, strings.NewReader(content))
|
||||
assert.NoError(t, err)
|
||||
if !exist {
|
||||
err := contentStore.Put(pointer, strings.NewReader(content))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
return pointer.Oid
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user