From d99f4ab0035631aacc44739af0a936e59daf83b7 Mon Sep 17 00:00:00 2001 From: Antoine GIRARD Date: Tue, 28 Nov 2017 21:58:37 +0100 Subject: [PATCH] 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 --- integrations/api_repo_lfs_locks_test.go | 176 ++++++++++++++++++ integrations/mysql.ini.tmpl | 3 +- integrations/pgsql.ini.tmpl | 2 +- integrations/sqlite.ini | 27 +-- models/error.go | 57 ++++++ models/lfs_lock.go | 146 +++++++++++++++ models/models.go | 1 + modules/lfs/locks.go | 236 ++++++++++++++++++++++++ routers/routes/routes.go | 6 + 9 files changed, 638 insertions(+), 16 deletions(-) create mode 100644 integrations/api_repo_lfs_locks_test.go create mode 100644 models/lfs_lock.go create mode 100644 modules/lfs/locks.go diff --git a/integrations/api_repo_lfs_locks_test.go b/integrations/api_repo_lfs_locks_test.go new file mode 100644 index 000000000..9fd796b6c --- /dev/null +++ b/integrations/api_repo_lfs_locks_test.go @@ -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) + } +} diff --git a/integrations/mysql.ini.tmpl b/integrations/mysql.ini.tmpl index fb1a4f581..e01362607 100644 --- a/integrations/mysql.ini.tmpl +++ b/integrations/mysql.ini.tmpl @@ -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 - diff --git a/integrations/pgsql.ini.tmpl b/integrations/pgsql.ini.tmpl index 01916af9a..853d6fb3b 100644 --- a/integrations/pgsql.ini.tmpl +++ b/integrations/pgsql.ini.tmpl @@ -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] diff --git a/integrations/sqlite.ini b/integrations/sqlite.ini index 8a3a5356b..b3462a19b 100644 --- a/integrations/sqlite.ini +++ b/integrations/sqlite.ini @@ -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] diff --git a/models/error.go b/models/error.go index 7ea4e9e2f..50d9cba17 100644 --- a/models/error.go +++ b/models/error.go @@ -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) +} + // __________ .__ __ // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | diff --git a/models/lfs_lock.go b/models/lfs_lock.go new file mode 100644 index 000000000..83811bc7b --- /dev/null +++ b/models/lfs_lock.go @@ -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 +} diff --git a/models/models.go b/models/models.go index 836a14db5..8a3850b6f 100644 --- a/models/models.go +++ b/models/models.go @@ -117,6 +117,7 @@ func init() { new(TrackedTime), new(DeletedBranch), new(RepoIndexerStatus), + new(LFSLock), ) gonicNames := []string{"SSL", "UID"} diff --git a/modules/lfs/locks.go b/modules/lfs/locks.go new file mode 100644 index 000000000..38d290e13 --- /dev/null +++ b/modules/lfs/locks.go @@ -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()}) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 9a42ef68d..2945c3147 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -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) })