diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /routers/api/v1/repo/mirror.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | routers/api/v1/repo/mirror.go | 449 |
1 files changed, 449 insertions, 0 deletions
diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go new file mode 100644 index 0000000..ae727fd --- /dev/null +++ b/routers/api/v1/repo/mirror.go @@ -0,0 +1,449 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "fmt" + "net/http" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/migrations" + mirror_service "code.gitea.io/gitea/services/mirror" +) + +// MirrorSync adds a mirrored repository to the sync queue +func MirrorSync(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync + // --- + // summary: Sync a mirrored repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo to sync + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to sync + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "413": + // "$ref": "#/responses/quotaExceeded" + + repo := ctx.Repo.Repository + + if !ctx.Repo.CanWrite(unit.TypeCode) { + ctx.Error(http.StatusForbidden, "MirrorSync", "Must have write access") + } + + if !setting.Mirror.Enabled { + ctx.Error(http.StatusBadRequest, "MirrorSync", "Mirror feature is disabled") + return + } + + if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil { + if errors.Is(err, repo_model.ErrMirrorNotExist) { + ctx.Error(http.StatusBadRequest, "MirrorSync", "Repository is not a mirror") + return + } + ctx.Error(http.StatusInternalServerError, "MirrorSync", err) + return + } + + mirror_service.AddPullMirrorToQueue(repo.ID) + + ctx.Status(http.StatusOK) +} + +// PushMirrorSync adds all push mirrored repositories to the sync queue +func PushMirrorSync(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync + // --- + // summary: Sync all push mirrored repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo to sync + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to sync + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/empty" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "413": + // "$ref": "#/responses/quotaExceeded" + + if !setting.Mirror.Enabled { + ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled") + return + } + // Get All push mirrors of a specific repo + pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) + if err != nil { + ctx.Error(http.StatusNotFound, "PushMirrorSync", err) + return + } + for _, mirror := range pushMirrors { + ok := mirror_service.SyncPushMirror(ctx, mirror.ID) + if !ok { + ctx.Error(http.StatusInternalServerError, "PushMirrorSync", "error occurred when syncing push mirror "+mirror.RemoteName) + return + } + } + + ctx.Status(http.StatusOK) +} + +// ListPushMirrors get list of push mirrors of a repository +func ListPushMirrors(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors + // --- + // summary: Get all push mirrors of the repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/PushMirrorList" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if !setting.Mirror.Enabled { + ctx.Error(http.StatusBadRequest, "GetPushMirrorsByRepoID", "Mirror feature is disabled") + return + } + + repo := ctx.Repo.Repository + // Get all push mirrors for the specified repository. + pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusNotFound, "GetPushMirrorsByRepoID", err) + return + } + + responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors)) + for _, mirror := range pushMirrors { + m, err := convert.ToPushMirror(ctx, mirror) + if err == nil { + responsePushMirrors = append(responsePushMirrors, m) + } + } + ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize) + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, responsePushMirrors) +} + +// GetPushMirrorByName get push mirror of a repository by name +func GetPushMirrorByName(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName + // --- + // summary: Get push mirror of the repository by remoteName + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: name + // in: path + // description: remote name of push mirror + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/PushMirror" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if !setting.Mirror.Enabled { + ctx.Error(http.StatusBadRequest, "GetPushMirrorByRemoteName", "Mirror feature is disabled") + return + } + + mirrorName := ctx.Params(":name") + // Get push mirror of a specific repo by remoteName + pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{ + RepoID: ctx.Repo.Repository.ID, + RemoteName: mirrorName, + }.ToConds()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetPushMirrors", err) + return + } else if !exist { + ctx.Error(http.StatusNotFound, "GetPushMirrors", nil) + return + } + + m, err := convert.ToPushMirror(ctx, pushMirror) + if err != nil { + ctx.ServerError("GetPushMirrorByRemoteName", err) + return + } + ctx.JSON(http.StatusOK, m) +} + +// AddPushMirror adds a push mirror to a repository +func AddPushMirror(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror + // --- + // summary: add a push mirror to the repository + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreatePushMirrorOption" + // responses: + // "200": + // "$ref": "#/responses/PushMirror" + // "403": + // "$ref": "#/responses/forbidden" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + // "413": + // "$ref": "#/responses/quotaExceeded" + + if !setting.Mirror.Enabled { + ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled") + return + } + + pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption) + CreatePushMirror(ctx, pushMirror) +} + +// DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName +func DeletePushMirrorByRemoteName(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror + // --- + // summary: deletes a push mirror from a repository by remoteName + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: name + // in: path + // description: remote name of the pushMirror + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "400": + // "$ref": "#/responses/error" + + if !setting.Mirror.Enabled { + ctx.Error(http.StatusBadRequest, "DeletePushMirrorByName", "Mirror feature is disabled") + return + } + + remoteName := ctx.Params(":name") + // Delete push mirror on repo by name. + err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) + if err != nil { + ctx.Error(http.StatusNotFound, "DeletePushMirrors", err) + return + } + ctx.Status(http.StatusNoContent) +} + +func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) { + repo := ctx.Repo.Repository + + interval, err := time.ParseDuration(mirrorOption.Interval) + if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { + ctx.Error(http.StatusBadRequest, "CreatePushMirror", err) + return + } + + if mirrorOption.UseSSH && !git.HasSSHExecutable { + ctx.Error(http.StatusBadRequest, "CreatePushMirror", "SSH authentication not available.") + return + } + + if mirrorOption.UseSSH && (mirrorOption.RemoteUsername != "" || mirrorOption.RemotePassword != "") { + ctx.Error(http.StatusBadRequest, "CreatePushMirror", "'use_ssh' is mutually exclusive with 'remote_username' and 'remote_passoword'") + return + } + + address, err := forms.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword) + if err == nil { + err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser) + } + if err != nil { + HandleRemoteAddressError(ctx, err) + return + } + + remoteSuffix, err := util.CryptoRandomString(10) + if err != nil { + ctx.ServerError("CryptoRandomString", err) + return + } + + remoteAddress, err := util.SanitizeURL(address) + if err != nil { + ctx.ServerError("SanitizeURL", err) + return + } + + pushMirror := &repo_model.PushMirror{ + RepoID: repo.ID, + Repo: repo, + RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix), + Interval: interval, + SyncOnCommit: mirrorOption.SyncOnCommit, + RemoteAddress: remoteAddress, + } + + var plainPrivateKey []byte + if mirrorOption.UseSSH { + publicKey, privateKey, err := util.GenerateSSHKeypair() + if err != nil { + ctx.ServerError("GenerateSSHKeypair", err) + return + } + plainPrivateKey = privateKey + pushMirror.PublicKey = string(publicKey) + } + + if err = db.Insert(ctx, pushMirror); err != nil { + ctx.ServerError("InsertPushMirror", err) + return + } + + if mirrorOption.UseSSH { + if err = pushMirror.SetPrivatekey(ctx, plainPrivateKey); err != nil { + ctx.ServerError("SetPrivatekey", err) + return + } + } + + // if the registration of the push mirrorOption fails remove it from the database + if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil { + if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil { + ctx.ServerError("DeletePushMirrors", err) + return + } + ctx.ServerError("AddPushMirrorRemote", err) + return + } + m, err := convert.ToPushMirror(ctx, pushMirror) + if err != nil { + ctx.ServerError("ToPushMirror", err) + return + } + ctx.JSON(http.StatusOK, m) +} + +func HandleRemoteAddressError(ctx *context.APIContext, err error) { + if models.IsErrInvalidCloneAddr(err) { + addrErr := err.(*models.ErrInvalidCloneAddr) + switch { + case addrErr.IsProtocolInvalid: + ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol") + case addrErr.IsURLError: + ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ") + case addrErr.IsPermissionDenied: + ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied") + default: + ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error") + } + return + } +} |