code.gitea.io/gitea@v1.22.3/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/setting"
    17  	api "code.gitea.io/gitea/modules/structs"
    18  	"code.gitea.io/gitea/modules/util"
    19  	"code.gitea.io/gitea/modules/web"
    20  	"code.gitea.io/gitea/routers/api/v1/utils"
    21  	"code.gitea.io/gitea/services/context"
    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(ctx, mirror)
   180  		if err == nil {
   181  			responsePushMirrors = append(responsePushMirrors, m)
   182  		}
   183  	}
   184  	ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize)
   185  	ctx.SetTotalCountHeader(count)
   186  	ctx.JSON(http.StatusOK, responsePushMirrors)
   187  }
   188  
   189  // GetPushMirrorByName get push mirror of a repository by name
   190  func GetPushMirrorByName(ctx *context.APIContext) {
   191  	// swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
   192  	// ---
   193  	// summary: Get push mirror of the repository by remoteName
   194  	// produces:
   195  	// - application/json
   196  	// parameters:
   197  	// - name: owner
   198  	//   in: path
   199  	//   description: owner of the repo
   200  	//   type: string
   201  	//   required: true
   202  	// - name: repo
   203  	//   in: path
   204  	//   description: name of the repo
   205  	//   type: string
   206  	//   required: true
   207  	// - name: name
   208  	//   in: path
   209  	//   description: remote name of push mirror
   210  	//   type: string
   211  	//   required: true
   212  	// responses:
   213  	//   "200":
   214  	//     "$ref": "#/responses/PushMirror"
   215  	//   "400":
   216  	//     "$ref": "#/responses/error"
   217  	//   "403":
   218  	//     "$ref": "#/responses/forbidden"
   219  	//   "404":
   220  	//     "$ref": "#/responses/notFound"
   221  
   222  	if !setting.Mirror.Enabled {
   223  		ctx.Error(http.StatusBadRequest, "GetPushMirrorByRemoteName", "Mirror feature is disabled")
   224  		return
   225  	}
   226  
   227  	mirrorName := ctx.Params(":name")
   228  	// Get push mirror of a specific repo by remoteName
   229  	pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{
   230  		RepoID:     ctx.Repo.Repository.ID,
   231  		RemoteName: mirrorName,
   232  	}.ToConds())
   233  	if err != nil {
   234  		ctx.Error(http.StatusInternalServerError, "GetPushMirrors", err)
   235  		return
   236  	} else if !exist {
   237  		ctx.Error(http.StatusNotFound, "GetPushMirrors", nil)
   238  		return
   239  	}
   240  
   241  	m, err := convert.ToPushMirror(ctx, pushMirror)
   242  	if err != nil {
   243  		ctx.ServerError("GetPushMirrorByRemoteName", err)
   244  		return
   245  	}
   246  	ctx.JSON(http.StatusOK, m)
   247  }
   248  
   249  // AddPushMirror adds a push mirror to a repository
   250  func AddPushMirror(ctx *context.APIContext) {
   251  	// swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
   252  	// ---
   253  	// summary: add a push mirror to the repository
   254  	// consumes:
   255  	// - application/json
   256  	// produces:
   257  	// - application/json
   258  	// parameters:
   259  	// - name: owner
   260  	//   in: path
   261  	//   description: owner of the repo
   262  	//   type: string
   263  	//   required: true
   264  	// - name: repo
   265  	//   in: path
   266  	//   description: name of the repo
   267  	//   type: string
   268  	//   required: true
   269  	// - name: body
   270  	//   in: body
   271  	//   schema:
   272  	//     "$ref": "#/definitions/CreatePushMirrorOption"
   273  	// responses:
   274  	//   "200":
   275  	//     "$ref": "#/responses/PushMirror"
   276  	//   "403":
   277  	//     "$ref": "#/responses/forbidden"
   278  	//   "400":
   279  	//     "$ref": "#/responses/error"
   280  	//   "404":
   281  	//     "$ref": "#/responses/notFound"
   282  
   283  	if !setting.Mirror.Enabled {
   284  		ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")
   285  		return
   286  	}
   287  
   288  	pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption)
   289  	CreatePushMirror(ctx, pushMirror)
   290  }
   291  
   292  // DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
   293  func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
   294  	// swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
   295  	// ---
   296  	// summary: deletes a push mirror from a repository by remoteName
   297  	// produces:
   298  	// - application/json
   299  	// parameters:
   300  	// - name: owner
   301  	//   in: path
   302  	//   description: owner of the repo
   303  	//   type: string
   304  	//   required: true
   305  	// - name: repo
   306  	//   in: path
   307  	//   description: name of the repo
   308  	//   type: string
   309  	//   required: true
   310  	// - name: name
   311  	//   in: path
   312  	//   description: remote name of the pushMirror
   313  	//   type: string
   314  	//   required: true
   315  	// responses:
   316  	//   "204":
   317  	//     "$ref": "#/responses/empty"
   318  	//   "404":
   319  	//     "$ref": "#/responses/notFound"
   320  	//   "400":
   321  	//     "$ref": "#/responses/error"
   322  
   323  	if !setting.Mirror.Enabled {
   324  		ctx.Error(http.StatusBadRequest, "DeletePushMirrorByName", "Mirror feature is disabled")
   325  		return
   326  	}
   327  
   328  	remoteName := ctx.Params(":name")
   329  	// Delete push mirror on repo by name.
   330  	err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
   331  	if err != nil {
   332  		ctx.Error(http.StatusNotFound, "DeletePushMirrors", err)
   333  		return
   334  	}
   335  	ctx.Status(http.StatusNoContent)
   336  }
   337  
   338  func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
   339  	repo := ctx.Repo.Repository
   340  
   341  	interval, err := time.ParseDuration(mirrorOption.Interval)
   342  	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
   343  		ctx.Error(http.StatusBadRequest, "CreatePushMirror", err)
   344  		return
   345  	}
   346  
   347  	address, err := forms.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
   348  	if err == nil {
   349  		err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
   350  	}
   351  	if err != nil {
   352  		HandleRemoteAddressError(ctx, err)
   353  		return
   354  	}
   355  
   356  	remoteSuffix, err := util.CryptoRandomString(10)
   357  	if err != nil {
   358  		ctx.ServerError("CryptoRandomString", err)
   359  		return
   360  	}
   361  
   362  	remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
   363  	if err != nil {
   364  		ctx.ServerError("SanitizeURL", err)
   365  		return
   366  	}
   367  
   368  	pushMirror := &repo_model.PushMirror{
   369  		RepoID:        repo.ID,
   370  		Repo:          repo,
   371  		RemoteName:    fmt.Sprintf("remote_mirror_%s", remoteSuffix),
   372  		Interval:      interval,
   373  		SyncOnCommit:  mirrorOption.SyncOnCommit,
   374  		RemoteAddress: remoteAddress,
   375  	}
   376  
   377  	if err = db.Insert(ctx, pushMirror); err != nil {
   378  		ctx.ServerError("InsertPushMirror", err)
   379  		return
   380  	}
   381  
   382  	// if the registration of the push mirrorOption fails remove it from the database
   383  	if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
   384  		if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
   385  			ctx.ServerError("DeletePushMirrors", err)
   386  			return
   387  		}
   388  		ctx.ServerError("AddPushMirrorRemote", err)
   389  		return
   390  	}
   391  	m, err := convert.ToPushMirror(ctx, pushMirror)
   392  	if err != nil {
   393  		ctx.ServerError("ToPushMirror", err)
   394  		return
   395  	}
   396  	ctx.JSON(http.StatusOK, m)
   397  }
   398  
   399  func HandleRemoteAddressError(ctx *context.APIContext, err error) {
   400  	if models.IsErrInvalidCloneAddr(err) {
   401  		addrErr := err.(*models.ErrInvalidCloneAddr)
   402  		switch {
   403  		case addrErr.IsProtocolInvalid:
   404  			ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol")
   405  		case addrErr.IsURLError:
   406  			ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ")
   407  		case addrErr.IsPermissionDenied:
   408  			ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied")
   409  		default:
   410  			ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error")
   411  		}
   412  		return
   413  	}
   414  }