code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/mirror.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"time"
    11  
    12  	"code.gitea.io/gitea/models"
    13  	"code.gitea.io/gitea/models/db"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	"code.gitea.io/gitea/models/unit"
    16  	"code.gitea.io/gitea/modules/context"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	api "code.gitea.io/gitea/modules/structs"
    19  	"code.gitea.io/gitea/modules/util"
    20  	"code.gitea.io/gitea/modules/web"
    21  	"code.gitea.io/gitea/routers/api/v1/utils"
    22  	"code.gitea.io/gitea/services/convert"
    23  	"code.gitea.io/gitea/services/forms"
    24  	"code.gitea.io/gitea/services/migrations"
    25  	mirror_service "code.gitea.io/gitea/services/mirror"
    26  )
    27  
    28  // MirrorSync adds a mirrored repository to the sync queue
    29  func MirrorSync(ctx *context.APIContext) {
    30  	// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
    31  	// ---
    32  	// summary: Sync a mirrored repository
    33  	// produces:
    34  	// - application/json
    35  	// parameters:
    36  	// - name: owner
    37  	//   in: path
    38  	//   description: owner of the repo to sync
    39  	//   type: string
    40  	//   required: true
    41  	// - name: repo
    42  	//   in: path
    43  	//   description: name of the repo to sync
    44  	//   type: string
    45  	//   required: true
    46  	// responses:
    47  	//   "200":
    48  	//     "$ref": "#/responses/empty"
    49  	//   "403":
    50  	//     "$ref": "#/responses/forbidden"
    51  	//   "404":
    52  	//     "$ref": "#/responses/notFound"
    53  
    54  	repo := ctx.Repo.Repository
    55  
    56  	if !ctx.Repo.CanWrite(unit.TypeCode) {
    57  		ctx.Error(http.StatusForbidden, "MirrorSync", "Must have write access")
    58  	}
    59  
    60  	if !setting.Mirror.Enabled {
    61  		ctx.Error(http.StatusBadRequest, "MirrorSync", "Mirror feature is disabled")
    62  		return
    63  	}
    64  
    65  	if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil {
    66  		if errors.Is(err, repo_model.ErrMirrorNotExist) {
    67  			ctx.Error(http.StatusBadRequest, "MirrorSync", "Repository is not a mirror")
    68  			return
    69  		}
    70  		ctx.Error(http.StatusInternalServerError, "MirrorSync", err)
    71  		return
    72  	}
    73  
    74  	mirror_service.AddPullMirrorToQueue(repo.ID)
    75  
    76  	ctx.Status(http.StatusOK)
    77  }
    78  
    79  // PushMirrorSync adds all push mirrored repositories to the sync queue
    80  func PushMirrorSync(ctx *context.APIContext) {
    81  	// swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync
    82  	// ---
    83  	// summary: Sync all push mirrored repository
    84  	// produces:
    85  	// - application/json
    86  	// parameters:
    87  	// - name: owner
    88  	//   in: path
    89  	//   description: owner of the repo to sync
    90  	//   type: string
    91  	//   required: true
    92  	// - name: repo
    93  	//   in: path
    94  	//   description: name of the repo to sync
    95  	//   type: string
    96  	//   required: true
    97  	// responses:
    98  	//   "200":
    99  	//     "$ref": "#/responses/empty"
   100  	//   "400":
   101  	//     "$ref": "#/responses/error"
   102  	//   "403":
   103  	//     "$ref": "#/responses/forbidden"
   104  	//   "404":
   105  	//     "$ref": "#/responses/notFound"
   106  
   107  	if !setting.Mirror.Enabled {
   108  		ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled")
   109  		return
   110  	}
   111  	// Get All push mirrors of a specific repo
   112  	pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
   113  	if err != nil {
   114  		ctx.Error(http.StatusNotFound, "PushMirrorSync", err)
   115  		return
   116  	}
   117  	for _, mirror := range pushMirrors {
   118  		ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
   119  		if !ok {
   120  			ctx.Error(http.StatusInternalServerError, "PushMirrorSync", "error occurred when syncing push mirror "+mirror.RemoteName)
   121  			return
   122  		}
   123  	}
   124  
   125  	ctx.Status(http.StatusOK)
   126  }
   127  
   128  // ListPushMirrors get list of push mirrors of a repository
   129  func ListPushMirrors(ctx *context.APIContext) {
   130  	// swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors
   131  	// ---
   132  	// summary: Get all push mirrors of the repository
   133  	// produces:
   134  	// - application/json
   135  	// parameters:
   136  	// - name: owner
   137  	//   in: path
   138  	//   description: owner of the repo
   139  	//   type: string
   140  	//   required: true
   141  	// - name: repo
   142  	//   in: path
   143  	//   description: name of the repo
   144  	//   type: string
   145  	//   required: true
   146  	// - name: page
   147  	//   in: query
   148  	//   description: page number of results to return (1-based)
   149  	//   type: integer
   150  	// - name: limit
   151  	//   in: query
   152  	//   description: page size of results
   153  	//   type: integer
   154  	// responses:
   155  	//   "200":
   156  	//     "$ref": "#/responses/PushMirrorList"
   157  	//   "400":
   158  	//     "$ref": "#/responses/error"
   159  	//   "403":
   160  	//     "$ref": "#/responses/forbidden"
   161  	//   "404":
   162  	//     "$ref": "#/responses/notFound"
   163  
   164  	if !setting.Mirror.Enabled {
   165  		ctx.Error(http.StatusBadRequest, "GetPushMirrorsByRepoID", "Mirror feature is disabled")
   166  		return
   167  	}
   168  
   169  	repo := ctx.Repo.Repository
   170  	// Get all push mirrors for the specified repository.
   171  	pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
   172  	if err != nil {
   173  		ctx.Error(http.StatusNotFound, "GetPushMirrorsByRepoID", err)
   174  		return
   175  	}
   176  
   177  	responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors))
   178  	for _, mirror := range pushMirrors {
   179  		m, err := convert.ToPushMirror(mirror)
   180  		if err == nil {
   181  			responsePushMirrors = append(responsePushMirrors, m)
   182  		}
   183  
   184  	}
   185  	ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize)
   186  	ctx.SetTotalCountHeader(count)
   187  	ctx.JSON(http.StatusOK, responsePushMirrors)
   188  }
   189  
   190  // GetPushMirrorByName get push mirror of a repository by name
   191  func GetPushMirrorByName(ctx *context.APIContext) {
   192  	// swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
   193  	// ---
   194  	// summary: Get push mirror of the repository by remoteName
   195  	// produces:
   196  	// - application/json
   197  	// parameters:
   198  	// - name: owner
   199  	//   in: path
   200  	//   description: owner of the repo
   201  	//   type: string
   202  	//   required: true
   203  	// - name: repo
   204  	//   in: path
   205  	//   description: name of the repo
   206  	//   type: string
   207  	//   required: true
   208  	// - name: name
   209  	//   in: path
   210  	//   description: remote name of push mirror
   211  	//   type: string
   212  	//   required: true
   213  	// responses:
   214  	//   "200":
   215  	//     "$ref": "#/responses/PushMirror"
   216  	//   "400":
   217  	//     "$ref": "#/responses/error"
   218  	//   "403":
   219  	//     "$ref": "#/responses/forbidden"
   220  	//   "404":
   221  	//     "$ref": "#/responses/notFound"
   222  
   223  	if !setting.Mirror.Enabled {
   224  		ctx.Error(http.StatusBadRequest, "GetPushMirrorByRemoteName", "Mirror feature is disabled")
   225  		return
   226  	}
   227  
   228  	mirrorName := ctx.Params(":name")
   229  	// Get push mirror of a specific repo by remoteName
   230  	pushMirror, err := repo_model.GetPushMirror(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: mirrorName})
   231  	if err != nil {
   232  		ctx.Error(http.StatusNotFound, "GetPushMirrors", err)
   233  		return
   234  	}
   235  	m, err := convert.ToPushMirror(pushMirror)
   236  	if err != nil {
   237  		ctx.ServerError("GetPushMirrorByRemoteName", err)
   238  		return
   239  	}
   240  	ctx.JSON(http.StatusOK, m)
   241  }
   242  
   243  // AddPushMirror adds a push mirror to a repository
   244  func AddPushMirror(ctx *context.APIContext) {
   245  	// swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
   246  	// ---
   247  	// summary: add a push mirror to the repository
   248  	// consumes:
   249  	// - application/json
   250  	// produces:
   251  	// - application/json
   252  	// parameters:
   253  	// - name: owner
   254  	//   in: path
   255  	//   description: owner of the repo
   256  	//   type: string
   257  	//   required: true
   258  	// - name: repo
   259  	//   in: path
   260  	//   description: name of the repo
   261  	//   type: string
   262  	//   required: true
   263  	// - name: body
   264  	//   in: body
   265  	//   schema:
   266  	//     "$ref": "#/definitions/CreatePushMirrorOption"
   267  	// responses:
   268  	//   "200":
   269  	//     "$ref": "#/responses/PushMirror"
   270  	//   "403":
   271  	//     "$ref": "#/responses/forbidden"
   272  	//   "400":
   273  	//     "$ref": "#/responses/error"
   274  	//   "404":
   275  	//     "$ref": "#/responses/notFound"
   276  
   277  	if !setting.Mirror.Enabled {
   278  		ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")
   279  		return
   280  	}
   281  
   282  	pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption)
   283  	CreatePushMirror(ctx, pushMirror)
   284  }
   285  
   286  // DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
   287  func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
   288  	// swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
   289  	// ---
   290  	// summary: deletes a push mirror from a repository by remoteName
   291  	// produces:
   292  	// - application/json
   293  	// parameters:
   294  	// - name: owner
   295  	//   in: path
   296  	//   description: owner of the repo
   297  	//   type: string
   298  	//   required: true
   299  	// - name: repo
   300  	//   in: path
   301  	//   description: name of the repo
   302  	//   type: string
   303  	//   required: true
   304  	// - name: name
   305  	//   in: path
   306  	//   description: remote name of the pushMirror
   307  	//   type: string
   308  	//   required: true
   309  	// responses:
   310  	//   "204":
   311  	//     "$ref": "#/responses/empty"
   312  	//   "404":
   313  	//     "$ref": "#/responses/notFound"
   314  	//   "400":
   315  	//     "$ref": "#/responses/error"
   316  
   317  	if !setting.Mirror.Enabled {
   318  		ctx.Error(http.StatusBadRequest, "DeletePushMirrorByName", "Mirror feature is disabled")
   319  		return
   320  	}
   321  
   322  	remoteName := ctx.Params(":name")
   323  	// Delete push mirror on repo by name.
   324  	err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
   325  	if err != nil {
   326  		ctx.Error(http.StatusNotFound, "DeletePushMirrors", err)
   327  		return
   328  	}
   329  	ctx.Status(http.StatusNoContent)
   330  }
   331  
   332  func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
   333  	repo := ctx.Repo.Repository
   334  
   335  	interval, err := time.ParseDuration(mirrorOption.Interval)
   336  	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
   337  		ctx.Error(http.StatusBadRequest, "CreatePushMirror", err)
   338  		return
   339  	}
   340  
   341  	address, err := forms.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
   342  	if err == nil {
   343  		err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
   344  	}
   345  	if err != nil {
   346  		HandleRemoteAddressError(ctx, err)
   347  		return
   348  	}
   349  
   350  	remoteSuffix, err := util.CryptoRandomString(10)
   351  	if err != nil {
   352  		ctx.ServerError("CryptoRandomString", err)
   353  		return
   354  	}
   355  
   356  	remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
   357  	if err != nil {
   358  		ctx.ServerError("SanitizeURL", err)
   359  		return
   360  	}
   361  
   362  	pushMirror := &repo_model.PushMirror{
   363  		RepoID:        repo.ID,
   364  		Repo:          repo,
   365  		RemoteName:    fmt.Sprintf("remote_mirror_%s", remoteSuffix),
   366  		Interval:      interval,
   367  		SyncOnCommit:  mirrorOption.SyncOnCommit,
   368  		RemoteAddress: remoteAddress,
   369  	}
   370  
   371  	if err = repo_model.InsertPushMirror(ctx, pushMirror); err != nil {
   372  		ctx.ServerError("InsertPushMirror", err)
   373  		return
   374  	}
   375  
   376  	// if the registration of the push mirrorOption fails remove it from the database
   377  	if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
   378  		if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
   379  			ctx.ServerError("DeletePushMirrors", err)
   380  		}
   381  		ctx.ServerError("AddPushMirrorRemote", err)
   382  		return
   383  	}
   384  	m, err := convert.ToPushMirror(pushMirror)
   385  	if err != nil {
   386  		ctx.ServerError("ToPushMirror", err)
   387  		return
   388  	}
   389  	ctx.JSON(http.StatusOK, m)
   390  }
   391  
   392  func HandleRemoteAddressError(ctx *context.APIContext, err error) {
   393  	if models.IsErrInvalidCloneAddr(err) {
   394  		addrErr := err.(*models.ErrInvalidCloneAddr)
   395  		switch {
   396  		case addrErr.IsProtocolInvalid:
   397  			ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol")
   398  		case addrErr.IsURLError:
   399  			ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ")
   400  		case addrErr.IsPermissionDenied:
   401  			ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied")
   402  		default:
   403  			ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error")
   404  		}
   405  		return
   406  	}
   407  }