diff options
author | KN4CK3R <KN4CK3R@users.noreply.github.com> | 2021-04-09 00:25:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-08 18:25:57 -0400 |
commit | c03e488e14fdaf1c0056952f40c5fc8124719a30 (patch) | |
tree | 22338add91196fad9f40f9a74033525ad8f591eb /services/lfs/locks.go | |
parent | f544414a232c148d4baf2e9d807f6cbffed67928 (diff) | |
download | gitea-c03e488e14fdaf1c0056952f40c5fc8124719a30.tar.gz gitea-c03e488e14fdaf1c0056952f40c5fc8124719a30.zip |
Add LFS Migration and Mirror (#14726)
* Implemented LFS client.
* Implemented scanning for pointer files.
* Implemented downloading of lfs files.
* Moved model-dependent code into services.
* Removed models dependency. Added TryReadPointerFromBuffer.
* Migrated code from service to module.
* Centralised storage creation.
* Removed dependency from models.
* Moved ContentStore into modules.
* Share structs between server and client.
* Moved method to services.
* Implemented lfs download on clone.
* Implemented LFS sync on clone and mirror update.
* Added form fields.
* Updated templates.
* Fixed condition.
* Use alternate endpoint.
* Added missing methods.
* Fixed typo and make linter happy.
* Detached pointer parser from gogit dependency.
* Fixed TestGetLFSRange test.
* Added context to support cancellation.
* Use ReadFull to probably read more data.
* Removed duplicated code from models.
* Moved scan implementation into pointer_scanner_nogogit.
* Changed method name.
* Added comments.
* Added more/specific log/error messages.
* Embedded lfs.Pointer into models.LFSMetaObject.
* Moved code from models to module.
* Moved code from models to module.
* Moved code from models to module.
* Reduced pointer usage.
* Embedded type.
* Use promoted fields.
* Fixed unexpected eof.
* Added unit tests.
* Implemented migration of local file paths.
* Show an error on invalid LFS endpoints.
* Hide settings if not used.
* Added LFS info to mirror struct.
* Fixed comment.
* Check LFS endpoint.
* Manage LFS settings from mirror page.
* Fixed selector.
* Adjusted selector.
* Added more tests.
* Added local filesystem migration test.
* Fixed typo.
* Reset settings.
* Added special windows path handling.
* Added unit test for HTTPClient.
* Added unit test for BasicTransferAdapter.
* Moved into util package.
* Test if LFS endpoint is allowed.
* Added support for git://
* Just use a static placeholder as the displayed url may be invalid.
* Reverted to original code.
* Added "Advanced Settings".
* Updated wording.
* Added discovery info link.
* Implemented suggestion.
* Fixed missing format parameter.
* Added Pointer.IsValid().
* Always remove model on error.
* Added suggestions.
* Use channel instead of array.
* Update routers/repo/migrate.go
* fmt
Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: zeripath <art27@cantab.net>
Diffstat (limited to 'services/lfs/locks.go')
-rw-r--r-- | services/lfs/locks.go | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/services/lfs/locks.go b/services/lfs/locks.go new file mode 100644 index 0000000000..6bbe43d36b --- /dev/null +++ b/services/lfs/locks.go @@ -0,0 +1,349 @@ +// 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 ( + "net/http" + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + lfs_module "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + jsoniter "github.com/json-iterator/go" +) + +//checkIsValidRequest check if it a valid request in case of bad request it write the response to ctx. +func checkIsValidRequest(ctx *context.Context) bool { + if !setting.LFS.StartServer { + log.Debug("Attempt to access LFS server but LFS server is disabled") + writeStatus(ctx, http.StatusNotFound) + return false + } + if !MetaMatcher(ctx.Req) { + log.Info("Attempt access LOCKs without accepting the correct media type: %s", lfs_module.MediaType) + writeStatus(ctx, http.StatusBadRequest) + return false + } + if !ctx.IsSigned { + user, _, _, err := parseToken(ctx.Req.Header.Get("Authorization")) + if err != nil { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + writeStatus(ctx, http.StatusUnauthorized) + return false + } + ctx.User = user + } + return true +} + +func handleLockListOut(ctx *context.Context, repo *models.Repository, lock *models.LFSLock, err error) { + if err != nil { + if models.IsErrLFSLockNotExist(err) { + ctx.JSON(http.StatusOK, api.LFSLockList{ + Locks: []*api.LFSLock{}, + }) + return + } + ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ + Message: "unable to list locks : Internal Server Error", + }) + return + } + if repo.ID != lock.RepoID { + ctx.JSON(http.StatusOK, api.LFSLockList{ + Locks: []*api.LFSLock{}, + }) + return + } + ctx.JSON(http.StatusOK, api.LFSLockList{ + Locks: []*api.LFSLock{convert.ToLFSLock(lock)}, + }) +} + +// GetListLockHandler list locks +func GetListLockHandler(ctx *context.Context) { + if !checkIsValidRequest(ctx) { + // Status is written in checkIsValidRequest + return + } + ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) + + rv, _ := unpack(ctx) + + repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo) + if err != nil { + log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, rv.Authorization, false) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ + Message: "You must have pull access to list locks", + }) + return + } + + cursor := ctx.QueryInt("cursor") + if cursor < 0 { + cursor = 0 + } + limit := ctx.QueryInt("limit") + if limit > setting.LFS.LocksPagingNum && setting.LFS.LocksPagingNum > 0 { + limit = setting.LFS.LocksPagingNum + } else if limit < 0 { + limit = 0 + } + 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(http.StatusBadRequest, api.LFSLockError{ + Message: "bad request : " + err.Error(), + }) + return + } + lock, err := models.GetLFSLockByID(v) + if err != nil && !models.IsErrLFSLockNotExist(err) { + log.Error("Unable to get lock with ID[%s]: Error: %v", v, err) + } + handleLockListOut(ctx, repository, lock, err) + return + } + + path := ctx.Query("path") + if path != "" { //Case where we request a specific id + lock, err := models.GetLFSLock(repository, path) + if err != nil && !models.IsErrLFSLockNotExist(err) { + log.Error("Unable to get lock for repository %-v with path %s: Error: %v", repository, path, err) + } + handleLockListOut(ctx, repository, lock, err) + return + } + + //If no query params path or id + lockList, err := models.GetLFSLockByRepoID(repository.ID, cursor, limit) + if err != nil { + log.Error("Unable to list locks for repository ID[%d]: Error: %v", repository.ID, err) + ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ + Message: "unable to list locks : Internal Server Error", + }) + return + } + lockListAPI := make([]*api.LFSLock, len(lockList)) + next := "" + for i, l := range lockList { + lockListAPI[i] = convert.ToLFSLock(l) + } + if limit > 0 && len(lockList) == limit { + next = strconv.Itoa(cursor + 1) + } + ctx.JSON(http.StatusOK, api.LFSLockList{ + Locks: lockListAPI, + Next: next, + }) +} + +// PostLockHandler create lock +func PostLockHandler(ctx *context.Context) { + if !checkIsValidRequest(ctx) { + // Status is written in checkIsValidRequest + return + } + ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) + + userName := ctx.Params("username") + repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git") + authorization := ctx.Req.Header.Get("Authorization") + + repository, err := models.GetRepositoryByOwnerAndName(userName, repoName) + if err != nil { + log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, authorization, true) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ + Message: "You must have push access to create locks", + }) + return + } + + var req api.LFSLockRequest + bodyReader := ctx.Req.Body + defer bodyReader.Close() + json := jsoniter.ConfigCompatibleWithStandardLibrary + dec := json.NewDecoder(bodyReader) + if err := dec.Decode(&req); err != nil { + log.Warn("Failed to decode lock request as json. Error: %v", err) + writeStatus(ctx, 400) + return + } + + lock, err := models.CreateLFSLock(&models.LFSLock{ + Repo: repository, + Path: req.Path, + Owner: ctx.User, + }) + if err != nil { + if models.IsErrLFSLockAlreadyExist(err) { + ctx.JSON(http.StatusConflict, api.LFSLockError{ + Lock: convert.ToLFSLock(lock), + Message: "already created lock", + }) + return + } + if models.IsErrLFSUnauthorizedAction(err) { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ + Message: "You must have push access to create locks : " + err.Error(), + }) + return + } + log.Error("Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v", repository, req.Path, ctx.User, err) + ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ + Message: "internal server error : Internal Server Error", + }) + return + } + ctx.JSON(http.StatusCreated, api.LFSLockResponse{Lock: convert.ToLFSLock(lock)}) +} + +// VerifyLockHandler list locks for verification +func VerifyLockHandler(ctx *context.Context) { + if !checkIsValidRequest(ctx) { + // Status is written in checkIsValidRequest + return + } + ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) + + userName := ctx.Params("username") + repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git") + authorization := ctx.Req.Header.Get("Authorization") + + repository, err := models.GetRepositoryByOwnerAndName(userName, repoName) + if err != nil { + log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, authorization, true) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ + Message: "You must have push access to verify locks", + }) + return + } + + cursor := ctx.QueryInt("cursor") + if cursor < 0 { + cursor = 0 + } + limit := ctx.QueryInt("limit") + if limit > setting.LFS.LocksPagingNum && setting.LFS.LocksPagingNum > 0 { + limit = setting.LFS.LocksPagingNum + } else if limit < 0 { + limit = 0 + } + lockList, err := models.GetLFSLockByRepoID(repository.ID, cursor, limit) + if err != nil { + log.Error("Unable to list locks for repository ID[%d]: Error: %v", repository.ID, err) + ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ + Message: "unable to list locks : Internal Server Error", + }) + return + } + next := "" + if limit > 0 && len(lockList) == limit { + next = strconv.Itoa(cursor + 1) + } + 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, convert.ToLFSLock(l)) + } else { + lockTheirsListAPI = append(lockTheirsListAPI, convert.ToLFSLock(l)) + } + } + ctx.JSON(http.StatusOK, api.LFSLockListVerify{ + Ours: lockOursListAPI, + Theirs: lockTheirsListAPI, + Next: next, + }) +} + +// UnLockHandler delete locks +func UnLockHandler(ctx *context.Context) { + if !checkIsValidRequest(ctx) { + // Status is written in checkIsValidRequest + return + } + ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) + + userName := ctx.Params("username") + repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git") + authorization := ctx.Req.Header.Get("Authorization") + + repository, err := models.GetRepositoryByOwnerAndName(userName, repoName) + if err != nil { + log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, authorization, true) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ + Message: "You must have push access to delete locks", + }) + return + } + + var req api.LFSLockDeleteRequest + bodyReader := ctx.Req.Body + defer bodyReader.Close() + json := jsoniter.ConfigCompatibleWithStandardLibrary + dec := json.NewDecoder(bodyReader) + if err := dec.Decode(&req); err != nil { + log.Warn("Failed to decode lock request as json. Error: %v", err) + writeStatus(ctx, 400) + return + } + + lock, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, req.Force) + if err != nil { + if models.IsErrLFSUnauthorizedAction(err) { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ + Message: "You must have push access to delete locks : " + err.Error(), + }) + return + } + log.Error("Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v", ctx.ParamsInt64("lid"), ctx.User, req.Force, err) + ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ + Message: "unable to delete lock : Internal Server Error", + }) + return + } + ctx.JSON(http.StatusOK, api.LFSLockResponse{Lock: convert.ToLFSLock(lock)}) +} |