code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/migrate.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "net/http" 11 "strings" 12 13 "code.gitea.io/gitea/models" 14 "code.gitea.io/gitea/models/db" 15 "code.gitea.io/gitea/models/organization" 16 "code.gitea.io/gitea/models/perm" 17 access_model "code.gitea.io/gitea/models/perm/access" 18 repo_model "code.gitea.io/gitea/models/repo" 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/context" 21 "code.gitea.io/gitea/modules/graceful" 22 "code.gitea.io/gitea/modules/lfs" 23 "code.gitea.io/gitea/modules/log" 24 base "code.gitea.io/gitea/modules/migration" 25 "code.gitea.io/gitea/modules/setting" 26 api "code.gitea.io/gitea/modules/structs" 27 "code.gitea.io/gitea/modules/util" 28 "code.gitea.io/gitea/modules/web" 29 "code.gitea.io/gitea/services/convert" 30 "code.gitea.io/gitea/services/forms" 31 "code.gitea.io/gitea/services/migrations" 32 notify_service "code.gitea.io/gitea/services/notify" 33 repo_service "code.gitea.io/gitea/services/repository" 34 ) 35 36 // Migrate migrate remote git repository to gitea 37 func Migrate(ctx *context.APIContext) { 38 // swagger:operation POST /repos/migrate repository repoMigrate 39 // --- 40 // summary: Migrate a remote git repository 41 // consumes: 42 // - application/json 43 // produces: 44 // - application/json 45 // parameters: 46 // - name: body 47 // in: body 48 // schema: 49 // "$ref": "#/definitions/MigrateRepoOptions" 50 // responses: 51 // "201": 52 // "$ref": "#/responses/Repository" 53 // "403": 54 // "$ref": "#/responses/forbidden" 55 // "409": 56 // description: The repository with the same name already exists. 57 // "422": 58 // "$ref": "#/responses/validationError" 59 60 form := web.GetForm(ctx).(*api.MigrateRepoOptions) 61 62 // get repoOwner 63 var ( 64 repoOwner *user_model.User 65 err error 66 ) 67 if len(form.RepoOwner) != 0 { 68 repoOwner, err = user_model.GetUserByName(ctx, form.RepoOwner) 69 } else if form.RepoOwnerID != 0 { 70 repoOwner, err = user_model.GetUserByID(ctx, form.RepoOwnerID) 71 } else { 72 repoOwner = ctx.Doer 73 } 74 if err != nil { 75 if user_model.IsErrUserNotExist(err) { 76 ctx.Error(http.StatusUnprocessableEntity, "", err) 77 } else { 78 ctx.Error(http.StatusInternalServerError, "GetUser", err) 79 } 80 return 81 } 82 83 if ctx.HasAPIError() { 84 ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) 85 return 86 } 87 88 if !ctx.Doer.IsAdmin { 89 if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID { 90 ctx.Error(http.StatusForbidden, "", "Given user is not an organization.") 91 return 92 } 93 94 if repoOwner.IsOrganization() { 95 // Check ownership of organization. 96 isOwner, err := organization.OrgFromUser(repoOwner).IsOwnedBy(ctx.Doer.ID) 97 if err != nil { 98 ctx.Error(http.StatusInternalServerError, "IsOwnedBy", err) 99 return 100 } else if !isOwner { 101 ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.") 102 return 103 } 104 } 105 } 106 107 remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword) 108 if err == nil { 109 err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer) 110 } 111 if err != nil { 112 handleRemoteAddrError(ctx, err) 113 return 114 } 115 116 gitServiceType := convert.ToGitServiceType(form.Service) 117 118 if form.Mirror && setting.Mirror.DisableNewPull { 119 ctx.Error(http.StatusForbidden, "MirrorsGlobalDisabled", fmt.Errorf("the site administrator has disabled the creation of new pull mirrors")) 120 return 121 } 122 123 if setting.Repository.DisableMigrations { 124 ctx.Error(http.StatusForbidden, "MigrationsGlobalDisabled", fmt.Errorf("the site administrator has disabled migrations")) 125 return 126 } 127 128 form.LFS = form.LFS && setting.LFS.StartServer 129 130 if form.LFS && len(form.LFSEndpoint) > 0 { 131 ep := lfs.DetermineEndpoint("", form.LFSEndpoint) 132 if ep == nil { 133 ctx.Error(http.StatusInternalServerError, "", ctx.Tr("repo.migrate.invalid_lfs_endpoint")) 134 return 135 } 136 err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) 137 if err != nil { 138 handleRemoteAddrError(ctx, err) 139 return 140 } 141 } 142 143 opts := migrations.MigrateOptions{ 144 CloneAddr: remoteAddr, 145 RepoName: form.RepoName, 146 Description: form.Description, 147 Private: form.Private || setting.Repository.ForcePrivate, 148 Mirror: form.Mirror, 149 LFS: form.LFS, 150 LFSEndpoint: form.LFSEndpoint, 151 AuthUsername: form.AuthUsername, 152 AuthPassword: form.AuthPassword, 153 AuthToken: form.AuthToken, 154 Wiki: form.Wiki, 155 Issues: form.Issues, 156 Milestones: form.Milestones, 157 Labels: form.Labels, 158 Comments: form.Issues || form.PullRequests, 159 PullRequests: form.PullRequests, 160 Releases: form.Releases, 161 GitServiceType: gitServiceType, 162 MirrorInterval: form.MirrorInterval, 163 } 164 if opts.Mirror { 165 opts.Issues = false 166 opts.Milestones = false 167 opts.Labels = false 168 opts.Comments = false 169 opts.PullRequests = false 170 opts.Releases = false 171 } 172 173 repo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{ 174 Name: opts.RepoName, 175 Description: opts.Description, 176 OriginalURL: form.CloneAddr, 177 GitServiceType: gitServiceType, 178 IsPrivate: opts.Private, 179 IsMirror: opts.Mirror, 180 Status: repo_model.RepositoryBeingMigrated, 181 }) 182 if err != nil { 183 handleMigrateError(ctx, repoOwner, remoteAddr, err) 184 return 185 } 186 187 opts.MigrateToRepoID = repo.ID 188 189 defer func() { 190 if e := recover(); e != nil { 191 var buf bytes.Buffer 192 fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2)) 193 194 err = errors.New(buf.String()) 195 } 196 197 if err == nil { 198 notify_service.MigrateRepository(ctx, ctx.Doer, repoOwner, repo) 199 return 200 } 201 202 if repo != nil { 203 if errDelete := repo_service.DeleteRepositoryDirectly(ctx, ctx.Doer, repoOwner.ID, repo.ID); errDelete != nil { 204 log.Error("DeleteRepository: %v", errDelete) 205 } 206 } 207 }() 208 209 if repo, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.Doer, repoOwner.Name, opts, nil); err != nil { 210 handleMigrateError(ctx, repoOwner, remoteAddr, err) 211 return 212 } 213 214 log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName) 215 ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeAdmin})) 216 } 217 218 func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, remoteAddr string, err error) { 219 switch { 220 case repo_model.IsErrRepoAlreadyExist(err): 221 ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") 222 case repo_model.IsErrRepoFilesAlreadyExist(err): 223 ctx.Error(http.StatusConflict, "", "Files already exist for this repository. Adopt them or delete them.") 224 case migrations.IsRateLimitError(err): 225 ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.") 226 case migrations.IsTwoFactorAuthError(err): 227 ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit required two factors authentication.") 228 case repo_model.IsErrReachLimitOfRepo(err): 229 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) 230 case db.IsErrNameReserved(err): 231 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(db.ErrNameReserved).Name)) 232 case db.IsErrNameCharsNotAllowed(err): 233 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(db.ErrNameCharsNotAllowed).Name)) 234 case db.IsErrNamePatternNotAllowed(err): 235 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern)) 236 case models.IsErrInvalidCloneAddr(err): 237 ctx.Error(http.StatusUnprocessableEntity, "", err) 238 case base.IsErrNotSupported(err): 239 ctx.Error(http.StatusUnprocessableEntity, "", err) 240 default: 241 err = util.SanitizeErrorCredentialURLs(err) 242 if strings.Contains(err.Error(), "Authentication failed") || 243 strings.Contains(err.Error(), "Bad credentials") || 244 strings.Contains(err.Error(), "could not read Username") { 245 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Authentication failed: %v.", err)) 246 } else if strings.Contains(err.Error(), "fatal:") { 247 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Migration failed: %v.", err)) 248 } else { 249 ctx.Error(http.StatusInternalServerError, "MigrateRepository", err) 250 } 251 } 252 } 253 254 func handleRemoteAddrError(ctx *context.APIContext, err error) { 255 if models.IsErrInvalidCloneAddr(err) { 256 addrErr := err.(*models.ErrInvalidCloneAddr) 257 switch { 258 case addrErr.IsURLError: 259 ctx.Error(http.StatusUnprocessableEntity, "", err) 260 case addrErr.IsPermissionDenied: 261 if addrErr.LocalPath { 262 ctx.Error(http.StatusUnprocessableEntity, "", "You are not allowed to import local repositories.") 263 } else { 264 ctx.Error(http.StatusUnprocessableEntity, "", "You can not import from disallowed hosts.") 265 } 266 case addrErr.IsInvalidPath: 267 ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.") 268 default: 269 ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error()) 270 } 271 } else { 272 ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", err) 273 } 274 }