mirror of
https://github.com/go-gitea/gitea
synced 2024-11-24 18:23:06 +01:00
Git LFS lock api (#2938)
* Implement routes * move to api/sdk and create model * Implement add + list * List return 200 empty list no 404 * Add verify lfs lock api * Add delete and start implementing auth control * Revert to code.gitea.io/sdk/gitea vendor * Apply needed check for all lfs locks route * Add simple tests * fix lint * Improve tests * Add delete test + fix * Add lfs ascii header * Various fixes from review + remove useless code + add more corner case testing * Remove repo link since only id is needed. Save a little of memory and cpu time. * Improve tests * Use TEXT column format for path + test * fix mispell * Use NewRequestWithJSON for POST tests * Clean path * Improve DB format * Revert uniquess repoid+path * (Re)-setup uniqueness + max path length * Fixed TEXT in place of VARCHAR * Settle back to maximum VARCHAR(3072) * Let place for repoid in key * Let place for repoid in key * Let place for repoid in key * Revert back
This commit is contained in:
parent
6ad4990a65
commit
d99f4ab003
9 changed files with 638 additions and 16 deletions
176
integrations/api_repo_lfs_locks_test.go
Normal file
176
integrations/api_repo_lfs_locks_test.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
// Copyright 2017 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 (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPILFSLocksNotStarted(t *testing.T) {
|
||||||
|
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, "GET", "/%s/%s/info/lfs/locks", user.Name, repo.Name)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
req = NewRequestf(t, "POST", "/%s/%s/info/lfs/locks", user.Name, repo.Name)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
req = NewRequestf(t, "GET", "/%s/%s/info/lfs/locks/verify", user.Name, repo.Name)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
req = NewRequestf(t, "GET", "/%s/%s/info/lfs/locks/10/unlock", user.Name, repo.Name)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPILFSLocksNotLogin(t *testing.T) {
|
||||||
|
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, "GET", "/%s/%s/info/lfs/locks", user.Name, repo.Name)
|
||||||
|
req.Header.Set("Accept", "application/vnd.git-lfs+json")
|
||||||
|
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
|
||||||
|
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
var lfsLockError api.LFSLockError
|
||||||
|
DecodeJSON(t, resp, &lfsLockError)
|
||||||
|
assert.Equal(t, "You must have pull access to list locks : User undefined doesn't have rigth to list for lfs lock [rid: 1]", lfsLockError.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPILFSLocksLogged(t *testing.T) {
|
||||||
|
prepareTestEnv(t)
|
||||||
|
setting.LFS.StartServer = true
|
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) //in org 3
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) //in org 3
|
||||||
|
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // own by org 3
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
user *models.User
|
||||||
|
repo *models.Repository
|
||||||
|
path string
|
||||||
|
httpResult int
|
||||||
|
addTime []int
|
||||||
|
}{
|
||||||
|
{user: user2, repo: repo1, path: "foo/bar.zip", httpResult: http.StatusCreated, addTime: []int{0}},
|
||||||
|
{user: user2, repo: repo1, path: "path/test", httpResult: http.StatusCreated, addTime: []int{0}},
|
||||||
|
{user: user2, repo: repo1, path: "path/test", httpResult: http.StatusConflict},
|
||||||
|
{user: user2, repo: repo1, path: "Foo/BaR.zip", httpResult: http.StatusConflict},
|
||||||
|
{user: user2, repo: repo1, path: "Foo/Test/../subFOlder/../Relative/../BaR.zip", httpResult: http.StatusConflict},
|
||||||
|
{user: user4, repo: repo1, path: "FoO/BaR.zip", httpResult: http.StatusForbidden},
|
||||||
|
{user: user4, repo: repo1, path: "path/test-user4", httpResult: http.StatusForbidden},
|
||||||
|
{user: user2, repo: repo1, path: "patH/Test-user4", httpResult: http.StatusCreated, addTime: []int{0}},
|
||||||
|
{user: user2, repo: repo1, path: "some/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/path", httpResult: http.StatusCreated, addTime: []int{0}},
|
||||||
|
|
||||||
|
{user: user2, repo: repo3, path: "test/foo/bar.zip", httpResult: http.StatusCreated, addTime: []int{1, 2}},
|
||||||
|
{user: user4, repo: repo3, path: "test/foo/bar.zip", httpResult: http.StatusConflict},
|
||||||
|
{user: user4, repo: repo3, path: "test/foo/bar.bin", httpResult: http.StatusCreated, addTime: []int{1, 2}},
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsTests := []struct {
|
||||||
|
user *models.User
|
||||||
|
repo *models.Repository
|
||||||
|
totalCount int
|
||||||
|
oursCount int
|
||||||
|
theirsCount int
|
||||||
|
locksOwners []*models.User
|
||||||
|
locksTimes []time.Time
|
||||||
|
}{
|
||||||
|
{user: user2, repo: repo1, totalCount: 4, oursCount: 4, theirsCount: 0, locksOwners: []*models.User{user2, user2, user2, user2}, locksTimes: []time.Time{}},
|
||||||
|
{user: user2, repo: repo3, totalCount: 2, oursCount: 1, theirsCount: 1, locksOwners: []*models.User{user2, user4}, locksTimes: []time.Time{}},
|
||||||
|
{user: user4, repo: repo3, totalCount: 2, oursCount: 1, theirsCount: 1, locksOwners: []*models.User{user2, user4}, locksTimes: []time.Time{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTests := []struct {
|
||||||
|
user *models.User
|
||||||
|
repo *models.Repository
|
||||||
|
lockID string
|
||||||
|
}{}
|
||||||
|
|
||||||
|
//create locks
|
||||||
|
for _, test := range tests {
|
||||||
|
session := loginUser(t, test.user.Name)
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/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")
|
||||||
|
session.MakeRequest(t, req, test.httpResult)
|
||||||
|
if len(test.addTime) > 0 {
|
||||||
|
for _, id := range test.addTime {
|
||||||
|
resultsTests[id].locksTimes = append(resultsTests[id].locksTimes, time.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//check creation
|
||||||
|
for _, test := range resultsTests {
|
||||||
|
session := loginUser(t, test.user.Name)
|
||||||
|
req := NewRequestf(t, "GET", "/%s/info/lfs/locks", test.repo.FullName())
|
||||||
|
req.Header.Set("Accept", "application/vnd.git-lfs+json")
|
||||||
|
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var lfsLocks api.LFSLockList
|
||||||
|
DecodeJSON(t, resp, &lfsLocks)
|
||||||
|
assert.Len(t, lfsLocks.Locks, test.totalCount)
|
||||||
|
for i, lock := range lfsLocks.Locks {
|
||||||
|
assert.EqualValues(t, test.locksOwners[i].DisplayName(), lock.Owner.Name)
|
||||||
|
assert.WithinDuration(t, test.locksTimes[i], lock.LockedAt, 1*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/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")
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var lfsLocksVerify api.LFSLockListVerify
|
||||||
|
DecodeJSON(t, resp, &lfsLocksVerify)
|
||||||
|
assert.Len(t, lfsLocksVerify.Ours, test.oursCount)
|
||||||
|
assert.Len(t, lfsLocksVerify.Theirs, test.theirsCount)
|
||||||
|
for _, lock := range lfsLocksVerify.Ours {
|
||||||
|
assert.EqualValues(t, test.user.DisplayName(), lock.Owner.Name)
|
||||||
|
deleteTests = append(deleteTests, struct {
|
||||||
|
user *models.User
|
||||||
|
repo *models.Repository
|
||||||
|
lockID string
|
||||||
|
}{test.user, test.repo, lock.ID})
|
||||||
|
}
|
||||||
|
for _, lock := range lfsLocksVerify.Theirs {
|
||||||
|
assert.NotEqual(t, test.user.DisplayName(), lock.Owner.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//remove all locks
|
||||||
|
for _, test := range deleteTests {
|
||||||
|
session := loginUser(t, test.user.Name)
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/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")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var lfsLockRep api.LFSLockResponse
|
||||||
|
DecodeJSON(t, resp, &lfsLockRep)
|
||||||
|
assert.Equal(t, test.lockID, lfsLockRep.Lock.ID)
|
||||||
|
assert.Equal(t, test.user.DisplayName(), lfsLockRep.Lock.Owner.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we don't have any lock
|
||||||
|
for _, test := range resultsTests {
|
||||||
|
session := loginUser(t, test.user.Name)
|
||||||
|
req := NewRequestf(t, "GET", "/%s/info/lfs/locks", test.repo.FullName())
|
||||||
|
req.Header.Set("Accept", "application/vnd.git-lfs+json")
|
||||||
|
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var lfsLocks api.LFSLockList
|
||||||
|
DecodeJSON(t, resp, &lfsLocks)
|
||||||
|
assert.Len(t, lfsLocks.Locks, 0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ HTTP_PORT = 3001
|
||||||
ROOT_URL = http://localhost:3001/
|
ROOT_URL = http://localhost:3001/
|
||||||
DISABLE_SSH = false
|
DISABLE_SSH = false
|
||||||
SSH_PORT = 22
|
SSH_PORT = 22
|
||||||
LFS_START_SERVER = false
|
LFS_START_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
|
@ -65,4 +65,3 @@ LEVEL = Debug
|
||||||
INSTALL_LOCK = true
|
INSTALL_LOCK = true
|
||||||
SECRET_KEY = 9pCviYTWSb
|
SECRET_KEY = 9pCviYTWSb
|
||||||
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
|
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ HTTP_PORT = 3002
|
||||||
ROOT_URL = http://localhost:3002/
|
ROOT_URL = http://localhost:3002/
|
||||||
DISABLE_SSH = false
|
DISABLE_SSH = false
|
||||||
SSH_PORT = 22
|
SSH_PORT = 22
|
||||||
LFS_START_SERVER = false
|
LFS_START_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
|
|
|
@ -22,8 +22,9 @@ HTTP_PORT = 3003
|
||||||
ROOT_URL = http://localhost:3003/
|
ROOT_URL = http://localhost:3003/
|
||||||
DISABLE_SSH = false
|
DISABLE_SSH = false
|
||||||
SSH_PORT = 22
|
SSH_PORT = 22
|
||||||
LFS_START_SERVER = false
|
LFS_START_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
|
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
ENABLED = false
|
ENABLED = false
|
||||||
|
|
|
@ -506,6 +506,63 @@ func (err ErrLastOrgOwner) Error() string {
|
||||||
return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
|
return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//.____ ____________________
|
||||||
|
//| | \_ _____/ _____/
|
||||||
|
//| | | __) \_____ \
|
||||||
|
//| |___| \ / \
|
||||||
|
//|_______ \___ / /_______ /
|
||||||
|
// \/ \/ \/
|
||||||
|
|
||||||
|
// ErrLFSLockNotExist represents a "LFSLockNotExist" kind of error.
|
||||||
|
type ErrLFSLockNotExist struct {
|
||||||
|
ID int64
|
||||||
|
RepoID int64
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrLFSLockNotExist checks if an error is a ErrLFSLockNotExist.
|
||||||
|
func IsErrLFSLockNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrLFSLockNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrLFSLockNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("lfs lock does not exist [id: %d, rid: %d, path: %s]", err.ID, err.RepoID, err.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrLFSLockUnauthorizedAction represents a "LFSLockUnauthorizedAction" kind of error.
|
||||||
|
type ErrLFSLockUnauthorizedAction struct {
|
||||||
|
RepoID int64
|
||||||
|
UserName string
|
||||||
|
Action string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrLFSLockUnauthorizedAction checks if an error is a ErrLFSLockUnauthorizedAction.
|
||||||
|
func IsErrLFSLockUnauthorizedAction(err error) bool {
|
||||||
|
_, ok := err.(ErrLFSLockUnauthorizedAction)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrLFSLockUnauthorizedAction) Error() string {
|
||||||
|
return fmt.Sprintf("User %s doesn't have rigth to %s for lfs lock [rid: %d]", err.UserName, err.Action, err.RepoID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error.
|
||||||
|
type ErrLFSLockAlreadyExist struct {
|
||||||
|
RepoID int64
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrLFSLockAlreadyExist checks if an error is a ErrLFSLockAlreadyExist.
|
||||||
|
func IsErrLFSLockAlreadyExist(err error) bool {
|
||||||
|
_, ok := err.(ErrLFSLockAlreadyExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrLFSLockAlreadyExist) Error() string {
|
||||||
|
return fmt.Sprintf("lfs lock already exists [rid: %d, path: %s]", err.RepoID, err.Path)
|
||||||
|
}
|
||||||
|
|
||||||
// __________ .__ __
|
// __________ .__ __
|
||||||
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
||||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
||||||
|
|
146
models/lfs_lock.go
Normal file
146
models/lfs_lock.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// Copyright 2017 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 models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LFSLock represents a git lfs lock of repository.
|
||||||
|
type LFSLock struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||||
|
Owner *User `xorm:"-"`
|
||||||
|
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||||
|
Path string `xorm:"TEXT"`
|
||||||
|
Created time.Time `xorm:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeInsert is invoked from XORM before inserting an object of this type.
|
||||||
|
func (l *LFSLock) BeforeInsert() {
|
||||||
|
l.OwnerID = l.Owner.ID
|
||||||
|
l.Path = cleanPath(l.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||||
|
func (l *LFSLock) AfterLoad() {
|
||||||
|
l.Owner, _ = GetUserByID(l.OwnerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPath(p string) string {
|
||||||
|
return strings.ToLower(path.Clean(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIFormat convert a Release to lfs.LFSLock
|
||||||
|
func (l *LFSLock) APIFormat() *api.LFSLock {
|
||||||
|
return &api.LFSLock{
|
||||||
|
ID: strconv.FormatInt(l.ID, 10),
|
||||||
|
Path: l.Path,
|
||||||
|
LockedAt: l.Created,
|
||||||
|
Owner: &api.LFSLockOwner{
|
||||||
|
Name: l.Owner.DisplayName(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLFSLock creates a new lock.
|
||||||
|
func CreateLFSLock(lock *LFSLock) (*LFSLock, error) {
|
||||||
|
err := CheckLFSAccessForRepo(lock.Owner, lock.RepoID, "create")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := GetLFSLock(lock.RepoID, lock.Path)
|
||||||
|
if err == nil {
|
||||||
|
return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path}
|
||||||
|
}
|
||||||
|
if !IsErrLFSLockNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = x.InsertOne(lock)
|
||||||
|
return lock, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLFSLock returns release by given path.
|
||||||
|
func GetLFSLock(repoID int64, path string) (*LFSLock, error) {
|
||||||
|
path = cleanPath(path)
|
||||||
|
rel := &LFSLock{RepoID: repoID, Path: path}
|
||||||
|
has, err := x.Get(rel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return nil, ErrLFSLockNotExist{0, repoID, path}
|
||||||
|
}
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLFSLockByID returns release by given id.
|
||||||
|
func GetLFSLockByID(id int64) (*LFSLock, error) {
|
||||||
|
lock := new(LFSLock)
|
||||||
|
has, err := x.ID(id).Get(lock)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrLFSLockNotExist{id, 0, ""}
|
||||||
|
}
|
||||||
|
return lock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLFSLockByRepoID returns a list of locks of repository.
|
||||||
|
func GetLFSLockByRepoID(repoID int64) (locks []*LFSLock, err error) {
|
||||||
|
err = x.Where("repo_id = ?", repoID).Find(&locks)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLFSLockByID deletes a lock by given ID.
|
||||||
|
func DeleteLFSLockByID(id int64, u *User, force bool) (*LFSLock, error) {
|
||||||
|
lock, err := GetLFSLockByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CheckLFSAccessForRepo(u, lock.RepoID, "delete")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force && u.ID != lock.OwnerID {
|
||||||
|
return nil, fmt.Errorf("user doesn't own lock and force flag is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = x.ID(id).Delete(new(LFSLock))
|
||||||
|
return lock, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//CheckLFSAccessForRepo check needed access mode base on action
|
||||||
|
func CheckLFSAccessForRepo(u *User, repoID int64, action string) error {
|
||||||
|
if u == nil {
|
||||||
|
return ErrLFSLockUnauthorizedAction{repoID, "undefined", action}
|
||||||
|
}
|
||||||
|
mode := AccessModeRead
|
||||||
|
if action == "create" || action == "delete" || action == "verify" {
|
||||||
|
mode = AccessModeWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := GetRepositoryByID(repoID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
has, err := HasAccess(u.ID, repo, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return ErrLFSLockUnauthorizedAction{repo.ID, u.DisplayName(), action}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -117,6 +117,7 @@ func init() {
|
||||||
new(TrackedTime),
|
new(TrackedTime),
|
||||||
new(DeletedBranch),
|
new(DeletedBranch),
|
||||||
new(RepoIndexerStatus),
|
new(RepoIndexerStatus),
|
||||||
|
new(LFSLock),
|
||||||
)
|
)
|
||||||
|
|
||||||
gonicNames := []string{"SSL", "UID"}
|
gonicNames := []string{"SSL", "UID"}
|
||||||
|
|
236
modules/lfs/locks.go
Normal file
236
modules/lfs/locks.go
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
// Copyright 2017 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
"gopkg.in/macaron.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkRequest(req macaron.Request) int {
|
||||||
|
if !setting.LFS.StartServer {
|
||||||
|
return 404
|
||||||
|
}
|
||||||
|
if !MetaMatcher(req) || req.Header.Get("Content-Type") != metaMediaType {
|
||||||
|
return 400
|
||||||
|
}
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLockListOut(ctx *context.Context, lock *models.LFSLock, err error) {
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrLFSLockNotExist(err) {
|
||||||
|
ctx.JSON(200, api.LFSLockList{
|
||||||
|
Locks: []*api.LFSLock{},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "unable to list locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Repo.Repository.ID != lock.RepoID {
|
||||||
|
ctx.JSON(200, api.LFSLockList{
|
||||||
|
Locks: []*api.LFSLock{},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(200, api.LFSLockList{
|
||||||
|
Locks: []*api.LFSLock{lock.APIFormat()},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListLockHandler list locks
|
||||||
|
func GetListLockHandler(ctx *context.Context) {
|
||||||
|
status := checkRequest(ctx.Req)
|
||||||
|
if status != 200 {
|
||||||
|
writeStatus(ctx, status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Resp.Header().Set("Content-Type", metaMediaType)
|
||||||
|
|
||||||
|
err := models.CheckLFSAccessForRepo(ctx.User, ctx.Repo.Repository.ID, "list")
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrLFSLockUnauthorizedAction(err) {
|
||||||
|
ctx.JSON(403, api.LFSLockError{
|
||||||
|
Message: "You must have pull access to list locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "unable to list lock : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//TODO handle query cursor and limit
|
||||||
|
id := ctx.Query("id")
|
||||||
|
if id != "" { //Case where we request a specific id
|
||||||
|
v, err := strconv.ParseInt(id, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, api.LFSLockError{
|
||||||
|
Message: "bad request : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lock, err := models.GetLFSLockByID(int64(v))
|
||||||
|
handleLockListOut(ctx, lock, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ctx.Query("path")
|
||||||
|
if path != "" { //Case where we request a specific id
|
||||||
|
lock, err := models.GetLFSLock(ctx.Repo.Repository.ID, path)
|
||||||
|
handleLockListOut(ctx, lock, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//If no query params path or id
|
||||||
|
lockList, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "unable to list locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lockListAPI := make([]*api.LFSLock, len(lockList))
|
||||||
|
for i, l := range lockList {
|
||||||
|
lockListAPI[i] = l.APIFormat()
|
||||||
|
}
|
||||||
|
ctx.JSON(200, api.LFSLockList{
|
||||||
|
Locks: lockListAPI,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostLockHandler create lock
|
||||||
|
func PostLockHandler(ctx *context.Context) {
|
||||||
|
status := checkRequest(ctx.Req)
|
||||||
|
if status != 200 {
|
||||||
|
writeStatus(ctx, status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Resp.Header().Set("Content-Type", metaMediaType)
|
||||||
|
|
||||||
|
var req api.LFSLockRequest
|
||||||
|
dec := json.NewDecoder(ctx.Req.Body().ReadCloser())
|
||||||
|
err := dec.Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeStatus(ctx, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lock, err := models.CreateLFSLock(&models.LFSLock{
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
Path: req.Path,
|
||||||
|
Owner: ctx.User,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrLFSLockAlreadyExist(err) {
|
||||||
|
ctx.JSON(409, api.LFSLockError{
|
||||||
|
Lock: lock.APIFormat(),
|
||||||
|
Message: "already created lock",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if models.IsErrLFSLockUnauthorizedAction(err) {
|
||||||
|
ctx.JSON(403, api.LFSLockError{
|
||||||
|
Message: "You must have push access to create locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "internal server error : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(201, api.LFSLockResponse{Lock: lock.APIFormat()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyLockHandler list locks for verification
|
||||||
|
func VerifyLockHandler(ctx *context.Context) {
|
||||||
|
status := checkRequest(ctx.Req)
|
||||||
|
if status != 200 {
|
||||||
|
writeStatus(ctx, status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Resp.Header().Set("Content-Type", metaMediaType)
|
||||||
|
|
||||||
|
err := models.CheckLFSAccessForRepo(ctx.User, ctx.Repo.Repository.ID, "verify")
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrLFSLockUnauthorizedAction(err) {
|
||||||
|
ctx.JSON(403, api.LFSLockError{
|
||||||
|
Message: "You must have push access to verify locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "unable to verify lock : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO handle body json cursor and limit
|
||||||
|
lockList, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "unable to list locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lockOursListAPI := make([]*api.LFSLock, 0, len(lockList))
|
||||||
|
lockTheirsListAPI := make([]*api.LFSLock, 0, len(lockList))
|
||||||
|
for _, l := range lockList {
|
||||||
|
if l.Owner.ID == ctx.User.ID {
|
||||||
|
lockOursListAPI = append(lockOursListAPI, l.APIFormat())
|
||||||
|
} else {
|
||||||
|
lockTheirsListAPI = append(lockTheirsListAPI, l.APIFormat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.JSON(200, api.LFSLockListVerify{
|
||||||
|
Ours: lockOursListAPI,
|
||||||
|
Theirs: lockTheirsListAPI,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnLockHandler delete locks
|
||||||
|
func UnLockHandler(ctx *context.Context) {
|
||||||
|
status := checkRequest(ctx.Req)
|
||||||
|
if status != 200 {
|
||||||
|
writeStatus(ctx, status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Resp.Header().Set("Content-Type", metaMediaType)
|
||||||
|
|
||||||
|
var req api.LFSLockDeleteRequest
|
||||||
|
dec := json.NewDecoder(ctx.Req.Body().ReadCloser())
|
||||||
|
err := dec.Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeStatus(ctx, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lock, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, req.Force)
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrLFSLockUnauthorizedAction(err) {
|
||||||
|
ctx.JSON(403, api.LFSLockError{
|
||||||
|
Message: "You must have push access to delete locks : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(500, api.LFSLockError{
|
||||||
|
Message: "unable to delete lock : " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(200, api.LFSLockResponse{Lock: lock.APIFormat()})
|
||||||
|
}
|
|
@ -685,6 +685,12 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Any("/objects/:oid", lfs.ObjectOidHandler)
|
m.Any("/objects/:oid", lfs.ObjectOidHandler)
|
||||||
m.Post("/objects", lfs.PostHandler)
|
m.Post("/objects", lfs.PostHandler)
|
||||||
m.Post("/verify", lfs.VerifyHandler)
|
m.Post("/verify", 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)
|
||||||
|
}, context.RepoAssignment())
|
||||||
m.Any("/*", func(ctx *context.Context) {
|
m.Any("/*", func(ctx *context.Context) {
|
||||||
ctx.Handle(404, "", nil)
|
ctx.Handle(404, "", nil)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue