diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index d1f2c750e3931..75730a58eec95 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -265,6 +265,8 @@ var migrations = []Migration{ NewMigration("update reactions constraint", updateReactionConstraint), // v160 -> v161 NewMigration("Add block on official review requests branch protection", addBlockOnOfficialReviewRequests), + // v161 -> v162 + NewMigration("Add process field to task table", addProcessToTask), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v161.go b/models/migrations/v161.go new file mode 100644 index 0000000000000..5eddbb331b3df --- /dev/null +++ b/models/migrations/v161.go @@ -0,0 +1,17 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addProcessToTask(x *xorm.Engine) error { + type Task struct { + Process string // process GUID and PID + } + + return x.Sync2(new(Task)) +} diff --git a/models/task.go b/models/task.go index b86314b44938a..2af55d96654be 100644 --- a/models/task.go +++ b/models/task.go @@ -31,6 +31,7 @@ type Task struct { PayloadContent string `xorm:"TEXT"` Errors string `xorm:"TEXT"` // if task failed, saved the error reason Created timeutil.TimeStamp `xorm:"created"` + Process string // process GUID and PID } // LoadRepo loads repository of the task @@ -114,6 +115,17 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) { return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name()) } +// GUIDandPID return GUID and PID of a running task +func (task *Task) GUIDandPID() (guid int64, pid int64) { + if task.Status != structs.TaskStatusRunning { + return 0, 0 + } + if _, err := fmt.Sscanf(task.Process, "%d/%d", &guid, &pid); err != nil { + return 0, 0 + } + return +} + // ErrTaskDoesNotExist represents a "TaskDoesNotExist" kind of error. type ErrTaskDoesNotExist struct { ID int64 diff --git a/modules/process/manager.go b/modules/process/manager.go index 27ed1d4964d15..cf108e2ce913b 100644 --- a/modules/process/manager.go +++ b/modules/process/manager.go @@ -15,6 +15,8 @@ import ( "sort" "sync" "time" + + gouuid "github.com/google/uuid" ) // TODO: This packages still uses a singleton for the Manager. @@ -44,6 +46,9 @@ type Manager struct { counter int64 processes map[int64]*Process + + // guid is a unique id for a gitea instance + guid int64 } // GetManager returns a Manager and initializes one as singleton if there's none yet @@ -51,11 +56,17 @@ func GetManager() *Manager { if manager == nil { manager = &Manager{ processes: make(map[int64]*Process), + guid: int64(gouuid.New().ID()), } } return manager } +// GUID return a unique id of a gitea instance +func (pm *Manager) GUID() int64 { + return pm.guid +} + // Add a process to the ProcessManager and returns its PID. func (pm *Manager) Add(description string, cancel context.CancelFunc) int64 { pm.mutex.Lock() diff --git a/modules/task/migrate.go b/modules/task/migrate.go index 57424abac38ca..85b621016a720 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -102,7 +102,8 @@ func runMigrateTask(t *models.Task) (err error) { t.StartTime = timeutil.TimeStampNow() t.Status = structs.TaskStatusRunning - if err = t.UpdateCols("start_time", "status"); err != nil { + t.Process = fmt.Sprintf("%d/%d", process.GetManager().GUID(), pid) + if err = t.UpdateCols("start_time", "status", "process"); err != nil { return } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ff54bcd3b35aa..2fa30f28a8eb7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -760,6 +760,8 @@ migrate.migrate_items_options = Access Token is required to migrate additional i migrated_from = Migrated from %[2]s migrated_from_fake = Migrated From %[1]s migrate.migrate = Migrate From %s +migrate.cancelled = The migration has been cancelled. +migrate.task_not_found = The migration task for %s not found. migrate.migrating = Migrating from %s ... migrate.migrating_failed = Migrating from %s failed. migrate.github.description = Migrating data from Github.com or Github Enterprise. diff --git a/routers/repo/migrate.go b/routers/repo/migrate.go index d843a043a7a53..b616f24e1e488 100644 --- a/routers/repo/migrate.go +++ b/routers/repo/migrate.go @@ -6,17 +6,22 @@ package repo import ( + "net/http" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations" + "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/task" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + repo_service "code.gitea.io/gitea/services/repository" ) const ( @@ -188,3 +193,53 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, &form) } + +// CancelMigration cancels a running migration +func CancelMigration(ctx *context.Context) { + if !ctx.Repo.IsAdmin() { + ctx.Error(http.StatusNotFound) + return + } + + if ctx.Repo.Repository.Status != models.RepositoryBeingMigrated { + ctx.Error(http.StatusConflict, "repo already migrated") + return + } + + guid := process.GetManager().GUID() + task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + if task.Status != structs.TaskStatusRunning { + task.Status = structs.TaskStatusStopped + task.EndTime = timeutil.TimeStampNow() + task.RepoID = 0 + + if err = task.UpdateCols("status", "repo_id", "end_time"); err != nil { + log.Error("Task UpdateCols failed: %v", err) + } + + if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil { + ctx.ServerError("DeleteRepository", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.migrate.cancelled")) + ctx.Redirect(ctx.Repo.Owner.DashboardLink()) + return + } + + tGUID, tPID := task.GUIDandPID() + if guid == tGUID { + log.Trace("Migration [%s] cancel PID [%d] ", ctx.Repo.Repository.FullName(), tPID) + process.GetManager().Cancel(tPID) + ctx.Flash.Success(ctx.Tr("repo.migrate.cancelled")) + ctx.Redirect(ctx.Repo.Owner.DashboardLink()) + return + } + + ctx.Flash.Error(ctx.Tr("repo.migrate.task_not_found", ctx.Repo.Repository.FullName())) + ctx.Redirect(ctx.Repo.Owner.DashboardLink()) +} diff --git a/routers/routes/macaron.go b/routers/routes/macaron.go index 170bc7d493dff..c2b877c8ac4a3 100644 --- a/routers/routes/macaron.go +++ b/routers/routes/macaron.go @@ -529,6 +529,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) { m.Get("/:username/:reponame/releases/download/:vTag/:fileName", ignSignIn, context.RepoAssignment(), repo.MustBeNotEmpty, repo.RedirectDownload) m.Group("/:username/:reponame", func() { + m.Post("/migrate/cancel", repo.CancelMigration) m.Group("/settings", func() { m.Combo("").Get(repo.Settings). Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost) diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl index a4ec15fa9a660..638e6d4343498 100644 --- a/templates/repo/migrate/migrating.tmpl +++ b/templates/repo/migrate/migrating.tmpl @@ -29,6 +29,17 @@ + {{if .Permission.IsAdmin}} +
+
+
+ {{.CsrfTokenHtml}} + +
+
+ {{end}}