* 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 backtags/v1.4.0-rc1
// 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) | |||||
} | |||||
} |
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] | ||||
INSTALL_LOCK = true | INSTALL_LOCK = true | ||||
SECRET_KEY = 9pCviYTWSb | SECRET_KEY = 9pCviYTWSb | ||||
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ | INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ | ||||
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] |
RUN_MODE = prod | RUN_MODE = prod | ||||
[database] | [database] | ||||
DB_TYPE = sqlite3 | |||||
PATH = :memory: | |||||
DB_TYPE = sqlite3 | |||||
PATH = :memory: | |||||
[indexer] | [indexer] | ||||
ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve | |||||
ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve | |||||
REPO_INDEXER_ENABLED = true | REPO_INDEXER_ENABLED = true | ||||
REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve | |||||
REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve | |||||
[repository] | [repository] | ||||
ROOT = integrations/gitea-integration-sqlite/gitea-repositories | ROOT = integrations/gitea-integration-sqlite/gitea-repositories | ||||
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 | ||||
[service] | [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 | DEFAULT_ALLOW_CREATE_ORGANIZATION = true | ||||
NO_REPLY_ADDRESS = noreply.example.org | |||||
NO_REPLY_ADDRESS = noreply.example.org | |||||
[picture] | [picture] | ||||
DISABLE_GRAVATAR = false | DISABLE_GRAVATAR = false | ||||
PROVIDER = file | PROVIDER = file | ||||
[log] | [log] | ||||
MODE = console,file | |||||
MODE = console,file | |||||
ROOT_PATH = sqlite-log | ROOT_PATH = sqlite-log | ||||
[log.console] | [log.console] |
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) | |||||
} | |||||
// __________ .__ __ | // __________ .__ __ | ||||
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. | // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. | ||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | | // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | |
// 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 | |||||
} |
new(TrackedTime), | new(TrackedTime), | ||||
new(DeletedBranch), | new(DeletedBranch), | ||||
new(RepoIndexerStatus), | new(RepoIndexerStatus), | ||||
new(LFSLock), | |||||
) | ) | ||||
gonicNames := []string{"SSL", "UID"} | gonicNames := []string{"SSL", "UID"} |
// 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()}) | |||||
} |
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) | ||||
}) | }) |