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
					
				|  | @ -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/ | ||||
| DISABLE_SSH      = false | ||||
| SSH_PORT         = 22 | ||||
| LFS_START_SERVER = false | ||||
| LFS_START_SERVER = true | ||||
| OFFLINE_MODE     = false | ||||
| 
 | ||||
| [mailer] | ||||
|  | @ -65,4 +65,3 @@ LEVEL = Debug | |||
| INSTALL_LOCK   = true | ||||
| SECRET_KEY     = 9pCviYTWSb | ||||
| INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ HTTP_PORT        = 3002 | |||
| ROOT_URL         = http://localhost:3002/ | ||||
| DISABLE_SSH      = false | ||||
| SSH_PORT         = 22 | ||||
| LFS_START_SERVER = false | ||||
| LFS_START_SERVER = true | ||||
| OFFLINE_MODE     = false | ||||
| 
 | ||||
| [mailer] | ||||
|  |  | |||
|  | @ -2,13 +2,13 @@ APP_NAME = Gitea: Git with a cup of tea | |||
| RUN_MODE = prod | ||||
| 
 | ||||
| [database] | ||||
| DB_TYPE  = sqlite3 | ||||
| PATH     = :memory: | ||||
| DB_TYPE = sqlite3 | ||||
| PATH    = :memory: | ||||
| 
 | ||||
| [indexer] | ||||
| ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve | ||||
| ISSUE_INDEXER_PATH   = integrations/indexers-sqlite/issues.bleve | ||||
| REPO_INDEXER_ENABLED = true | ||||
| REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve | ||||
| REPO_INDEXER_PATH    = integrations/indexers-sqlite/repos.bleve | ||||
| 
 | ||||
| [repository] | ||||
| ROOT = integrations/gitea-integration-sqlite/gitea-repositories | ||||
|  | @ -22,21 +22,22 @@ HTTP_PORT        = 3003 | |||
| ROOT_URL         = http://localhost:3003/ | ||||
| DISABLE_SSH      = false | ||||
| SSH_PORT         = 22 | ||||
| LFS_START_SERVER = false | ||||
| LFS_START_SERVER = true | ||||
| OFFLINE_MODE     = false | ||||
| LFS_JWT_SECRET   = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | ||||
| 
 | ||||
| [mailer] | ||||
| ENABLED = false | ||||
| 
 | ||||
| [service] | ||||
| REGISTER_EMAIL_CONFIRM     = false | ||||
| ENABLE_NOTIFY_MAIL         = false | ||||
| DISABLE_REGISTRATION       = false | ||||
| ENABLE_CAPTCHA             = false | ||||
| REQUIRE_SIGNIN_VIEW        = false | ||||
| DEFAULT_KEEP_EMAIL_PRIVATE = false | ||||
| REGISTER_EMAIL_CONFIRM            = false | ||||
| ENABLE_NOTIFY_MAIL                = false | ||||
| DISABLE_REGISTRATION              = false | ||||
| ENABLE_CAPTCHA                    = false | ||||
| REQUIRE_SIGNIN_VIEW               = false | ||||
| DEFAULT_KEEP_EMAIL_PRIVATE        = false | ||||
| DEFAULT_ALLOW_CREATE_ORGANIZATION = true | ||||
| NO_REPLY_ADDRESS           = noreply.example.org | ||||
| NO_REPLY_ADDRESS                  = noreply.example.org | ||||
| 
 | ||||
| [picture] | ||||
| DISABLE_GRAVATAR        = false | ||||
|  | @ -46,7 +47,7 @@ ENABLE_FEDERATED_AVATAR = false | |||
| PROVIDER = file | ||||
| 
 | ||||
| [log] | ||||
| MODE = console,file | ||||
| MODE      = console,file | ||||
| ROOT_PATH = sqlite-log | ||||
| 
 | ||||
| [log.console] | ||||
|  |  | |||
|  | @ -506,6 +506,63 @@ func (err ErrLastOrgOwner) Error() string { | |||
| 	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) | ||||
| } | ||||
| 
 | ||||
| // __________                           .__  __
 | ||||
| // \______   \ ____ ______   ____  _____|__|/  |_  ___________ ___.__.
 | ||||
| //  |       _// __ \\____ \ /  _ \/  ___/  \   __\/  _ \_  __ <   |  |
 | ||||
|  |  | |||
|  | @ -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(DeletedBranch), | ||||
| 		new(RepoIndexerStatus), | ||||
| 		new(LFSLock), | ||||
| 	) | ||||
| 
 | ||||
| 	gonicNames := []string{"SSL", "UID"} | ||||
|  |  | |||
|  | @ -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.Post("/objects", lfs.PostHandler) | ||||
| 				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) { | ||||
| 					ctx.Handle(404, "", nil) | ||||
| 				}) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue