mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-03-11 17:48:44 -05:00
feat(cli): reorganize repair commands under unified 'vikunja repair' parent (#2300)
Consolidate four scattered repair/maintenance CLI commands into a unified `vikunja repair` parent command with subcommands.
This commit is contained in:
40
pkg/cmd/repair.go
Normal file
40
pkg/cmd/repair.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(repairCmd)
|
||||
}
|
||||
|
||||
var repairCmd = &cobra.Command{
|
||||
Use: "repair",
|
||||
Short: "Repair and fix data integrity issues",
|
||||
Long: `The repair command provides subcommands to detect and fix various
|
||||
data integrity issues in your Vikunja installation.
|
||||
|
||||
Available repair operations:
|
||||
task-positions - Fix duplicate task positions in project views
|
||||
projects - Fix orphaned projects with missing parents
|
||||
file-mime-types - Detect and set MIME types for files
|
||||
orphan-positions - Remove orphaned task position records
|
||||
|
||||
Most subcommands support --dry-run to preview changes without applying them.`,
|
||||
}
|
||||
@@ -26,33 +26,44 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(repairFileMimeTypesCmd)
|
||||
repairFileMimeTypesCmd.Flags().Bool("dry-run", false, "Preview repairs without making changes")
|
||||
repairCmd.AddCommand(repairFileMimeTypesCmd)
|
||||
}
|
||||
|
||||
var repairFileMimeTypesCmd = &cobra.Command{
|
||||
Use: "repair-file-mime-types",
|
||||
Use: "file-mime-types",
|
||||
Short: "Detect and set MIME types for all files that have none",
|
||||
Long: `Scans all files in the database that have no MIME type set,
|
||||
detects the type from the stored file content, and updates the database.
|
||||
|
||||
This is useful after upgrading from a version that did not store MIME types
|
||||
on file creation. Only files with an empty or NULL mime column are affected.`,
|
||||
on file creation. Only files with an empty or NULL mime column are affected.
|
||||
|
||||
Use --dry-run to preview what would be fixed without making changes.`,
|
||||
PreRun: func(_ *cobra.Command, _ []string) {
|
||||
initialize.FullInitWithoutAsync()
|
||||
},
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
result, err := files.RepairFileMimeTypes(s)
|
||||
if dryRun {
|
||||
log.Infof("Running in dry-run mode - no changes will be made")
|
||||
}
|
||||
|
||||
result, err := files.RepairFileMimeTypes(s, dryRun)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to repair file MIME types: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
log.Errorf("Failed to commit changes: %s", err)
|
||||
return
|
||||
if !dryRun {
|
||||
if err := s.Commit(); err != nil {
|
||||
log.Errorf("Failed to commit changes: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Repair complete:")
|
||||
|
||||
@@ -26,29 +26,44 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(deleteOrphanTaskPositions)
|
||||
repairOrphanPositionsCmd.Flags().Bool("dry-run", false, "Preview repairs without making changes")
|
||||
repairCmd.AddCommand(repairOrphanPositionsCmd)
|
||||
}
|
||||
|
||||
var deleteOrphanTaskPositions = &cobra.Command{
|
||||
Use: "delete-orphan-task-positions",
|
||||
Short: "Removes all task positions for tasks or project views which don't exist anymore.",
|
||||
var repairOrphanPositionsCmd = &cobra.Command{
|
||||
Use: "orphan-positions",
|
||||
Short: "Remove orphaned task position records for deleted tasks or views",
|
||||
Long: `Removes all task position records that reference tasks or project views
|
||||
which no longer exist in the database.
|
||||
|
||||
This can happen when tasks or views are deleted but their position records
|
||||
are not fully cleaned up.
|
||||
|
||||
Use --dry-run to preview what would be deleted without making changes.`,
|
||||
PreRun: func(_ *cobra.Command, _ []string) {
|
||||
initialize.FullInitWithoutAsync()
|
||||
},
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
count, err := models.DeleteOrphanedTaskPositions(s)
|
||||
if dryRun {
|
||||
log.Infof("Running in dry-run mode - no changes will be made")
|
||||
}
|
||||
|
||||
count, err := models.DeleteOrphanedTaskPositions(s, dryRun)
|
||||
if err != nil {
|
||||
log.Errorf("Could not delete orphaned task positions: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
log.Errorf("Could not commit orphaned task position deletion: %s", err)
|
||||
return
|
||||
if !dryRun {
|
||||
if err := s.Commit(); err != nil {
|
||||
log.Errorf("Could not commit orphaned task position deletion: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
@@ -56,6 +71,10 @@ var deleteOrphanTaskPositions = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Successfully deleted %d orphaned task positions.", count)
|
||||
if dryRun {
|
||||
log.Infof("Would delete %d orphaned task positions.", count)
|
||||
} else {
|
||||
log.Infof("Successfully deleted %d orphaned task positions.", count)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -27,11 +27,11 @@ import (
|
||||
|
||||
func init() {
|
||||
repairProjectsCmd.Flags().Bool("dry-run", false, "Preview repairs without making changes")
|
||||
rootCmd.AddCommand(repairProjectsCmd)
|
||||
repairCmd.AddCommand(repairProjectsCmd)
|
||||
}
|
||||
|
||||
var repairProjectsCmd = &cobra.Command{
|
||||
Use: "repair-projects",
|
||||
Use: "projects",
|
||||
Short: "Repair orphaned projects whose parent project no longer exists",
|
||||
Long: `Finds projects whose parent_project_id references a project that no longer
|
||||
exists in the database and re-parents them to the top level (parent_project_id = 0).
|
||||
|
||||
@@ -27,11 +27,11 @@ import (
|
||||
|
||||
func init() {
|
||||
repairTaskPositionsCmd.Flags().Bool("dry-run", false, "Preview repairs without making changes")
|
||||
rootCmd.AddCommand(repairTaskPositionsCmd)
|
||||
repairCmd.AddCommand(repairTaskPositionsCmd)
|
||||
}
|
||||
|
||||
var repairTaskPositionsCmd = &cobra.Command{
|
||||
Use: "repair-task-positions",
|
||||
Use: "task-positions",
|
||||
Short: "Detect and repair duplicate task positions across all views",
|
||||
Long: `Scans all project views for tasks with duplicate position values and repairs them.
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ type RepairMimeTypesResult struct {
|
||||
|
||||
// RepairFileMimeTypes finds all files with no MIME type set, detects it from
|
||||
// the stored file content, and updates the database.
|
||||
func RepairFileMimeTypes(s *xorm.Session) (*RepairMimeTypesResult, error) {
|
||||
// If dryRun is true, it reports what would be fixed without making changes.
|
||||
func RepairFileMimeTypes(s *xorm.Session, dryRun bool) (*RepairMimeTypesResult, error) {
|
||||
var files []*File
|
||||
err := s.Where("mime = '' OR mime IS NULL").Find(&files)
|
||||
if err != nil {
|
||||
@@ -73,13 +74,15 @@ func RepairFileMimeTypes(s *xorm.Session) (*RepairMimeTypesResult, error) {
|
||||
}
|
||||
|
||||
f.Mime = mime.String()
|
||||
_, err = s.ID(f.ID).Cols("mime").Update(f)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("file %d: failed to update mime type: %s", f.ID, err)
|
||||
log.Errorf("file %d: failed to update mime type: %s", f.ID, err)
|
||||
result.Errors = append(result.Errors, msg)
|
||||
_ = bar.Add(1)
|
||||
continue
|
||||
if !dryRun {
|
||||
_, err = s.ID(f.ID).Cols("mime").Update(f)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("file %d: failed to update mime type: %s", f.ID, err)
|
||||
log.Errorf("file %d: failed to update mime type: %s", f.ID, err)
|
||||
result.Errors = append(result.Errors, msg)
|
||||
_ = bar.Add(1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result.Updated++
|
||||
|
||||
@@ -338,10 +338,17 @@ func calculateNewPositionForTask(s *xorm.Session, a web.Auth, t *Task, view *Pro
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DeleteOrphanedTaskPositions(s *xorm.Session) (count int64, err error) {
|
||||
return s.
|
||||
Where("task_id not in (select id from tasks) OR project_view_id not in (select id from project_views)").
|
||||
Delete(&TaskPosition{})
|
||||
// DeleteOrphanedTaskPositions removes task position records that reference
|
||||
// tasks or project views that no longer exist.
|
||||
// If dryRun is true, it counts the orphaned records without deleting them.
|
||||
func DeleteOrphanedTaskPositions(s *xorm.Session, dryRun bool) (count int64, err error) {
|
||||
whereClause := "task_id not in (select id from tasks) OR project_view_id not in (select id from project_views)"
|
||||
|
||||
if dryRun {
|
||||
return s.Where(whereClause).Count(&TaskPosition{})
|
||||
}
|
||||
|
||||
return s.Where(whereClause).Delete(&TaskPosition{})
|
||||
}
|
||||
|
||||
// createPositionsForTasksInView creates position records for tasks that don't have them.
|
||||
|
||||
@@ -71,13 +71,14 @@ func TestDoPostWithHeaders_GivesUpAfter3Retries(t *testing.T) {
|
||||
if !strings.Contains(err.Error(), expectedBody) {
|
||||
t.Errorf("expected error message to contain response body %q, got: %s", expectedBody, err.Error())
|
||||
}
|
||||
if resp == nil {
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
t.Fatal("expected response to be returned with error, got nil")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", resp.StatusCode)
|
||||
}
|
||||
if attempts.Load() != 3 {
|
||||
t.Errorf("expected 3 attempts, got %d", attempts.Load())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user