You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

migrate.go 9.1KB

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