code.gitea.io/gitea@v1.22.3/routers/web/repo/migrate.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2020 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models"
    13  	admin_model "code.gitea.io/gitea/models/admin"
    14  	"code.gitea.io/gitea/models/db"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/json"
    19  	"code.gitea.io/gitea/modules/lfs"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/setting"
    22  	"code.gitea.io/gitea/modules/structs"
    23  	"code.gitea.io/gitea/modules/util"
    24  	"code.gitea.io/gitea/modules/web"
    25  	"code.gitea.io/gitea/services/context"
    26  	"code.gitea.io/gitea/services/forms"
    27  	"code.gitea.io/gitea/services/migrations"
    28  	"code.gitea.io/gitea/services/task"
    29  )
    30  
    31  const (
    32  	tplMigrate base.TplName = "repo/migrate/migrate"
    33  )
    34  
    35  // Migrate render migration of repository page
    36  func Migrate(ctx *context.Context) {
    37  	if setting.Repository.DisableMigrations {
    38  		ctx.Error(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
    39  		return
    40  	}
    41  
    42  	serviceType := structs.GitServiceType(ctx.FormInt("service_type"))
    43  
    44  	setMigrationContextData(ctx, serviceType)
    45  
    46  	if serviceType == 0 {
    47  		ctx.Data["Org"] = ctx.FormString("org")
    48  		ctx.Data["Mirror"] = ctx.FormString("mirror")
    49  
    50  		ctx.HTML(http.StatusOK, tplMigrate)
    51  		return
    52  	}
    53  
    54  	ctx.Data["private"] = getRepoPrivate(ctx)
    55  	ctx.Data["mirror"] = ctx.FormString("mirror") == "1"
    56  	ctx.Data["lfs"] = ctx.FormString("lfs") == "1"
    57  	ctx.Data["wiki"] = ctx.FormString("wiki") == "1"
    58  	ctx.Data["milestones"] = ctx.FormString("milestones") == "1"
    59  	ctx.Data["labels"] = ctx.FormString("labels") == "1"
    60  	ctx.Data["issues"] = ctx.FormString("issues") == "1"
    61  	ctx.Data["pull_requests"] = ctx.FormString("pull_requests") == "1"
    62  	ctx.Data["releases"] = ctx.FormString("releases") == "1"
    63  
    64  	ctxUser := checkContextUser(ctx, ctx.FormInt64("org"))
    65  	if ctx.Written() {
    66  		return
    67  	}
    68  	ctx.Data["ContextUser"] = ctxUser
    69  
    70  	ctx.HTML(http.StatusOK, base.TplName("repo/migrate/"+serviceType.Name()))
    71  }
    72  
    73  func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form *forms.MigrateRepoForm) {
    74  	if setting.Repository.DisableMigrations {
    75  		ctx.Error(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
    76  		return
    77  	}
    78  
    79  	switch {
    80  	case migrations.IsRateLimitError(err):
    81  		ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
    82  	case migrations.IsTwoFactorAuthError(err):
    83  		ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
    84  	case repo_model.IsErrReachLimitOfRepo(err):
    85  		maxCreationLimit := owner.MaxCreationLimit()
    86  		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
    87  		ctx.RenderWithErr(msg, tpl, form)
    88  	case repo_model.IsErrRepoAlreadyExist(err):
    89  		ctx.Data["Err_RepoName"] = true
    90  		ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
    91  	case repo_model.IsErrRepoFilesAlreadyExist(err):
    92  		ctx.Data["Err_RepoName"] = true
    93  		switch {
    94  		case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
    95  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
    96  		case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
    97  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
    98  		case setting.Repository.AllowDeleteOfUnadoptedRepositories:
    99  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
   100  		default:
   101  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
   102  		}
   103  	case db.IsErrNameReserved(err):
   104  		ctx.Data["Err_RepoName"] = true
   105  		ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
   106  	case db.IsErrNamePatternNotAllowed(err):
   107  		ctx.Data["Err_RepoName"] = true
   108  		ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
   109  	default:
   110  		err = util.SanitizeErrorCredentialURLs(err)
   111  		if strings.Contains(err.Error(), "Authentication failed") ||
   112  			strings.Contains(err.Error(), "Bad credentials") ||
   113  			strings.Contains(err.Error(), "could not read Username") {
   114  			ctx.Data["Err_Auth"] = true
   115  			ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
   116  		} else if strings.Contains(err.Error(), "fatal:") {
   117  			ctx.Data["Err_CloneAddr"] = true
   118  			ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
   119  		} else {
   120  			ctx.ServerError(name, err)
   121  		}
   122  	}
   123  }
   124  
   125  func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplName, form *forms.MigrateRepoForm) {
   126  	if models.IsErrInvalidCloneAddr(err) {
   127  		addrErr := err.(*models.ErrInvalidCloneAddr)
   128  		switch {
   129  		case addrErr.IsProtocolInvalid:
   130  			ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
   131  		case addrErr.IsURLError:
   132  			ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
   133  		case addrErr.IsPermissionDenied:
   134  			if addrErr.LocalPath {
   135  				ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
   136  			} else {
   137  				ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
   138  			}
   139  		case addrErr.IsInvalidPath:
   140  			ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
   141  		default:
   142  			log.Error("Error whilst updating url: %v", err)
   143  			ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
   144  		}
   145  	} else {
   146  		log.Error("Error whilst updating url: %v", err)
   147  		ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
   148  	}
   149  }
   150  
   151  // MigratePost response for migrating from external git repository
   152  func MigratePost(ctx *context.Context) {
   153  	form := web.GetForm(ctx).(*forms.MigrateRepoForm)
   154  	if setting.Repository.DisableMigrations {
   155  		ctx.Error(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
   156  		return
   157  	}
   158  
   159  	if form.Mirror && setting.Mirror.DisableNewPull {
   160  		ctx.Error(http.StatusBadRequest, "MigratePost: the site administrator has disabled creation of new mirrors")
   161  		return
   162  	}
   163  
   164  	setMigrationContextData(ctx, form.Service)
   165  
   166  	ctxUser := checkContextUser(ctx, form.UID)
   167  	if ctx.Written() {
   168  		return
   169  	}
   170  	ctx.Data["ContextUser"] = ctxUser
   171  
   172  	tpl := base.TplName("repo/migrate/" + form.Service.Name())
   173  
   174  	if ctx.HasError() {
   175  		ctx.HTML(http.StatusOK, tpl)
   176  		return
   177  	}
   178  
   179  	remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
   180  	if err == nil {
   181  		err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
   182  	}
   183  	if err != nil {
   184  		ctx.Data["Err_CloneAddr"] = true
   185  		handleMigrateRemoteAddrError(ctx, err, tpl, form)
   186  		return
   187  	}
   188  
   189  	form.LFS = form.LFS && setting.LFS.StartServer
   190  
   191  	if form.LFS && len(form.LFSEndpoint) > 0 {
   192  		ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
   193  		if ep == nil {
   194  			ctx.Data["Err_LFSEndpoint"] = true
   195  			ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
   196  			return
   197  		}
   198  		err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
   199  		if err != nil {
   200  			ctx.Data["Err_LFSEndpoint"] = true
   201  			handleMigrateRemoteAddrError(ctx, err, tpl, form)
   202  			return
   203  		}
   204  	}
   205  
   206  	opts := migrations.MigrateOptions{
   207  		OriginalURL:    form.CloneAddr,
   208  		GitServiceType: form.Service,
   209  		CloneAddr:      remoteAddr,
   210  		RepoName:       form.RepoName,
   211  		Description:    form.Description,
   212  		Private:        form.Private || setting.Repository.ForcePrivate,
   213  		Mirror:         form.Mirror,
   214  		LFS:            form.LFS,
   215  		LFSEndpoint:    form.LFSEndpoint,
   216  		AuthUsername:   form.AuthUsername,
   217  		AuthPassword:   form.AuthPassword,
   218  		AuthToken:      form.AuthToken,
   219  		Wiki:           form.Wiki,
   220  		Issues:         form.Issues,
   221  		Milestones:     form.Milestones,
   222  		Labels:         form.Labels,
   223  		Comments:       form.Issues || form.PullRequests,
   224  		PullRequests:   form.PullRequests,
   225  		Releases:       form.Releases,
   226  	}
   227  	if opts.Mirror {
   228  		opts.Issues = false
   229  		opts.Milestones = false
   230  		opts.Labels = false
   231  		opts.Comments = false
   232  		opts.PullRequests = false
   233  		opts.Releases = false
   234  	}
   235  
   236  	err = repo_model.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
   237  	if err != nil {
   238  		handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
   239  		return
   240  	}
   241  
   242  	err = task.MigrateRepository(ctx, ctx.Doer, ctxUser, opts)
   243  	if err == nil {
   244  		ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(opts.RepoName))
   245  		return
   246  	}
   247  
   248  	handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
   249  }
   250  
   251  func setMigrationContextData(ctx *context.Context, serviceType structs.GitServiceType) {
   252  	ctx.Data["Title"] = ctx.Tr("new_migrate")
   253  
   254  	ctx.Data["LFSActive"] = setting.LFS.StartServer
   255  	ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
   256  	ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
   257  
   258  	// Plain git should be first
   259  	ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
   260  	ctx.Data["service"] = serviceType
   261  }
   262  
   263  func MigrateRetryPost(ctx *context.Context) {
   264  	if err := task.RetryMigrateTask(ctx, ctx.Repo.Repository.ID); err != nil {
   265  		log.Error("Retry task failed: %v", err)
   266  		ctx.ServerError("task.RetryMigrateTask", err)
   267  		return
   268  	}
   269  	ctx.JSONOK()
   270  }
   271  
   272  func MigrateCancelPost(ctx *context.Context) {
   273  	migratingTask, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
   274  	if err != nil {
   275  		log.Error("GetMigratingTask: %v", err)
   276  		ctx.Redirect(ctx.Repo.Repository.Link())
   277  		return
   278  	}
   279  	if migratingTask.Status == structs.TaskStatusRunning {
   280  		taskUpdate := &admin_model.Task{ID: migratingTask.ID, Status: structs.TaskStatusFailed, Message: "canceled"}
   281  		if err = taskUpdate.UpdateCols(ctx, "status", "message"); err != nil {
   282  			ctx.ServerError("task.UpdateCols", err)
   283  			return
   284  		}
   285  	}
   286  	ctx.Redirect(ctx.Repo.Repository.Link())
   287  }
   288  
   289  // MigrateStatus returns migrate task's status
   290  func MigrateStatus(ctx *context.Context) {
   291  	task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
   292  	if err != nil {
   293  		if admin_model.IsErrTaskDoesNotExist(err) {
   294  			ctx.JSON(http.StatusNotFound, map[string]any{
   295  				"err": "task does not exist or you do not have access to this task",
   296  			})
   297  			return
   298  		}
   299  		log.Error("GetMigratingTask: %v", err)
   300  		ctx.JSON(http.StatusInternalServerError, map[string]any{
   301  			"err": http.StatusText(http.StatusInternalServerError),
   302  		})
   303  		return
   304  	}
   305  
   306  	message := task.Message
   307  
   308  	if task.Message != "" && task.Message[0] == '{' {
   309  		// assume message is actually a translatable string
   310  		var translatableMessage admin_model.TranslatableMessage
   311  		if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
   312  			translatableMessage = admin_model.TranslatableMessage{
   313  				Format: "migrate.migrating_failed.error",
   314  				Args:   []any{task.Message},
   315  			}
   316  		}
   317  		message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
   318  	}
   319  
   320  	ctx.JSON(http.StatusOK, map[string]any{
   321  		"status":  task.Status,
   322  		"message": message,
   323  	})
   324  }