🏡 index : quine-global/quintodrome.git

package cmd

import (
	"context"
	"database/sql"
	"fmt"
	"io"
	"os"

	"github.com/navidrome/navidrome/db"
	"github.com/spf13/cobra"
)

func init() {
	rootCmd.AddCommand(doctorCmd)
}

var doctorCmd = &cobra.Command{
	Use:   "doctor",
	Short: "Check your Navidrome installation for problems",
	Long: "Run read-only health checks and report what was found. Checks the database for " +
		"corruption and foreign key violations, and reports whether 'navidrome search rebuild' " +
		"can fix what it finds. This command never alters your data",
	Run: func(cmd *cobra.Command, _ []string) {
		runDoctor(cmd.Context())
	},
}

func runDoctor(ctx context.Context) {
	requireExistingDB()

	healthy := doctor(ctx, db.Db(), os.Stdout)
	db.Close(ctx)
	if !healthy {
		os.Exit(1)
	}
}

const recoveryAdvice = "Restore a backup (navidrome backup restore), or try SQLite's '.recover' command."

func printFindings(out io.Writer, check, noun string, items []string) {
	fmt.Fprintf(out, "%s reported %d %s:\n", check, len(items), noun)
	for _, item := range items {
		fmt.Fprintln(out, "  "+item)
	}
}

func doctor(ctx context.Context, database *sql.DB, out io.Writer) bool {
	healthy := true

	fmt.Fprintln(out, "Checking database integrity...")
	issues, truncated, err := db.IntegrityCheck(ctx, database)
	switch {
	case err != nil:
		fmt.Fprintln(out, "The integrity check could not complete: "+err.Error())
		fmt.Fprintln(out, recoveryAdvice)
		return false
	case len(issues) == 0:
		fmt.Fprintln(out, "Integrity check passed.")
	default:
		healthy = false
		printFindings(out, "Integrity check", "issue(s)", issues)
		switch {
		case truncated:
			fmt.Fprintln(out, "The integrity check stopped at its limit, so the damage may reach further than listed.")
			fmt.Fprintln(out, recoveryAdvice)
		case db.IsFTSCorruptionOnly(issues):
			fmt.Fprintln(out, "Corruption is limited to the search index. Run 'navidrome search rebuild' to fix it.")
		default:
			fmt.Fprintln(out, "Corruption is not limited to the search index, and cannot be repaired automatically.")
			fmt.Fprintln(out, recoveryAdvice)
		}
	}

	fmt.Fprintln(out, "Checking foreign keys...")
	violations, err := db.ForeignKeyCheck(ctx, database)
	switch {
	case err != nil:
		healthy = false
		fmt.Fprintln(out, "The foreign key check could not complete: "+err.Error())
	case len(violations) == 0:
		fmt.Fprintln(out, "Foreign key check passed.")
	default:
		healthy = false
		lines := make([]string, 0, len(violations))
		for _, v := range violations {
			lines = append(lines,
				fmt.Sprintf("%s: %d row(s) reference missing rows in %s", v.Table, v.Count, v.Parent))
		}
		printFindings(out, "Foreign key check", "violation(s)", lines)
		fmt.Fprintln(out, "These are orphaned rows, not corruption. 'navidrome scan -f' clears some of them "+
			"in library data; the rest have to be removed by hand.")
	}

	if healthy {
		fmt.Fprintln(out, "Database is healthy.")
	}
	return healthy
}