feat: add registry repair framework
- add shared registry validation service - add transaction-safe repair CLI - refactor doctor to use validator - improve restore integrity checks - add registry repair integration tests - strengthen global slug consistency
This commit is contained in:
1 parent
2761863c14
commit
f4947489af
12 files changed
+796
-255
No files matched your search
+52
-15
@@ -101,32 +101,69 @@ pub async fn run(
|
||||
Connection::open(&users_db_path),
|
||||
) {
|
||||
(Ok(sys_conn), Ok(usr_conn)) => {
|
||||
match crate::db::users::verify_global_slug_registry_integrity(
|
||||
match crate::services::registry_validator::RegistryValidator::scan(
|
||||
&sys_conn,
|
||||
&usr_conn,
|
||||
&config.data_dir,
|
||||
None,
|
||||
) {
|
||||
Ok((errors, warnings)) => {
|
||||
if errors.is_empty() && warnings.is_empty() {
|
||||
Ok(issues) => {
|
||||
if issues.is_empty() {
|
||||
println!(" Status: HEALTHY (no issues found)");
|
||||
} else {
|
||||
if !errors.is_empty() {
|
||||
println!(" Errors (Action Required):");
|
||||
for err in &errors {
|
||||
println!(" - {}", err);
|
||||
}
|
||||
all_healthy = false;
|
||||
}
|
||||
if !warnings.is_empty() {
|
||||
println!(" Warnings (Attention Needed):");
|
||||
for warn in &warnings {
|
||||
println!(" - {}", warn);
|
||||
println!(" Status: ISSUES DETECTED");
|
||||
all_healthy = false;
|
||||
|
||||
for issue in &issues {
|
||||
println!();
|
||||
println!("ERROR");
|
||||
println!();
|
||||
println!("Slug:");
|
||||
println!(" {}", issue.slug);
|
||||
println!();
|
||||
println!("Type:");
|
||||
println!(
|
||||
" {}",
|
||||
if issue.target_type == "url" {
|
||||
"URL"
|
||||
} else if issue.target_type == "page" {
|
||||
"Landing Page"
|
||||
} else {
|
||||
&issue.target_type
|
||||
}
|
||||
);
|
||||
println!();
|
||||
println!("Owner:");
|
||||
println!(" User ID {}", issue.owner_user_id);
|
||||
println!();
|
||||
println!("Database:");
|
||||
println!(" {}", issue.database_path.display());
|
||||
println!();
|
||||
println!("Target UUID:");
|
||||
println!(" {}", issue.target_id);
|
||||
println!();
|
||||
println!("Issue:");
|
||||
println!(" {:?}", issue.issue_type);
|
||||
println!();
|
||||
println!("Description:");
|
||||
println!(" {}", issue.description);
|
||||
println!();
|
||||
println!("Suggested Repair:");
|
||||
println!();
|
||||
if issue.slug != "*" {
|
||||
println!(
|
||||
" bzod repair registry --slug {} --dry-run",
|
||||
issue.slug
|
||||
);
|
||||
} else {
|
||||
println!(" bzod repair registry --dry-run");
|
||||
}
|
||||
println!("--------------------");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Status: ERROR running integrity check: {}", e);
|
||||
println!(" Status: ERROR running registry scan: {}", e);
|
||||
all_healthy = false;
|
||||
}
|
||||
}
|
||||
|
||||
+30
-10
@@ -1,26 +1,26 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
pub mod admin_migrate;
|
||||
pub mod backup;
|
||||
pub mod backup_user;
|
||||
pub mod create_admin;
|
||||
pub mod create_user;
|
||||
pub mod delete_user;
|
||||
pub mod disable_user;
|
||||
pub mod doctor;
|
||||
pub mod enable_user;
|
||||
pub mod expand;
|
||||
pub mod list_users;
|
||||
pub mod migrate;
|
||||
pub mod repair;
|
||||
pub mod reset_password;
|
||||
pub mod restore;
|
||||
pub mod restore_user;
|
||||
pub mod serve;
|
||||
pub mod shorten;
|
||||
pub mod stats;
|
||||
pub mod validate;
|
||||
|
||||
pub mod admin_migrate;
|
||||
pub mod backup_user;
|
||||
pub mod create_user;
|
||||
pub mod delete_user;
|
||||
pub mod disable_user;
|
||||
pub mod enable_user;
|
||||
pub mod list_users;
|
||||
pub mod reset_password;
|
||||
pub mod restore_user;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "bzod")]
|
||||
#[command(about = "BZOD - Personal Redirector & Landing Page Platform")]
|
||||
@@ -179,4 +179,24 @@ pub enum Commands {
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
},
|
||||
/// Repair registry and database inconsistencies
|
||||
Repair {
|
||||
#[command(subcommand)]
|
||||
command: RepairCommands,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
pub enum RepairCommands {
|
||||
/// Repair Global Slug Registry inconsistencies
|
||||
Registry {
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
#[arg(long)]
|
||||
slug: Option<String>,
|
||||
#[arg(long)]
|
||||
data_dir: Option<String>,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use crate::cli::RepairCommands;
|
||||
use crate::config::Config;
|
||||
use crate::services::registry_validator::{RegistryIssueType, RegistryValidator};
|
||||
use rusqlite::Connection;
|
||||
use std::path::PathBuf;
|
||||
use tracing::info;
|
||||
|
||||
pub async fn run(
|
||||
command: RepairCommands,
|
||||
mut config: Config,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match command {
|
||||
RepairCommands::Registry {
|
||||
dry_run,
|
||||
force,
|
||||
slug,
|
||||
data_dir,
|
||||
} => {
|
||||
if let Some(d) = data_dir {
|
||||
config.data_dir = PathBuf::from(d);
|
||||
}
|
||||
|
||||
if !dry_run && !force {
|
||||
println!("Error: You must specify either --dry-run or --force");
|
||||
return Ok(());
|
||||
}
|
||||
if dry_run && force {
|
||||
println!("Error: Cannot specify both --dry-run and --force");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let admin_dir = config.data_dir.join("admin");
|
||||
let system_db_path = admin_dir.join("system.db");
|
||||
let users_db_path = admin_dir.join("users.db");
|
||||
|
||||
if !system_db_path.exists() || !users_db_path.exists() {
|
||||
println!("Error: system.db or users.db not found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut sys_conn = Connection::open(&system_db_path)?;
|
||||
let usr_conn = Connection::open(&users_db_path)?;
|
||||
|
||||
let slug_filter = slug.as_deref();
|
||||
|
||||
if dry_run {
|
||||
println!("BZOD Registry Repair\n");
|
||||
println!("Scanning Global Slug Registry...");
|
||||
|
||||
let issues =
|
||||
RegistryValidator::scan(&sys_conn, &usr_conn, &config.data_dir, slug_filter)?;
|
||||
let orphaned = issues
|
||||
.into_iter()
|
||||
.filter(|i| {
|
||||
matches!(
|
||||
i.issue_type,
|
||||
RegistryIssueType::MissingTarget
|
||||
| RegistryIssueType::MissingDatabase
|
||||
| RegistryIssueType::MissingOwner
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let orphaned_pages = orphaned.iter().filter(|i| i.target_type == "page").count();
|
||||
let orphaned_urls = orphaned.iter().filter(|i| i.target_type == "url").count();
|
||||
|
||||
println!("\nDetected:");
|
||||
println!("\nPages:\n {} orphaned", orphaned_pages);
|
||||
println!("\nURLs:\n {} orphaned", orphaned_urls);
|
||||
|
||||
if !orphaned.is_empty() {
|
||||
println!("\nThe following entries would be removed:");
|
||||
for issue in &orphaned {
|
||||
println!("\n{}\n {}", issue.target_type.to_uppercase(), issue.slug);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nNo changes have been made.");
|
||||
println!(
|
||||
"\nRun again with:\n\n bzod repair registry --force{}",
|
||||
if let Some(s) = slug_filter {
|
||||
format!(" --slug {}", s)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
info!(
|
||||
"Registry Repair Started. Scanned. Orphaned Pages: {}, Orphaned URLs: {}. Duration: {:?}",
|
||||
orphaned_pages, orphaned_urls, start_time.elapsed()
|
||||
);
|
||||
} else if force {
|
||||
let tx = sys_conn.transaction()?;
|
||||
|
||||
let issues =
|
||||
RegistryValidator::scan(&tx, &usr_conn, &config.data_dir, slug_filter)?;
|
||||
let orphaned = issues
|
||||
.into_iter()
|
||||
.filter(|i| {
|
||||
matches!(
|
||||
i.issue_type,
|
||||
RegistryIssueType::MissingTarget
|
||||
| RegistryIssueType::MissingDatabase
|
||||
| RegistryIssueType::MissingOwner
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let orphaned_pages = orphaned.iter().filter(|i| i.target_type == "page").count();
|
||||
let orphaned_urls = orphaned.iter().filter(|i| i.target_type == "url").count();
|
||||
|
||||
if orphaned.is_empty() {
|
||||
println!("No repairs required.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(s) = slug_filter {
|
||||
println!("Checking slug:\n\n{}\n", s);
|
||||
if let Some(issue) = orphaned.first() {
|
||||
println!("Owner:\n\n{}\n", issue.owner_user_id);
|
||||
println!("Status:\n\nOrphaned\n");
|
||||
}
|
||||
}
|
||||
|
||||
let mut removed_count = 0;
|
||||
for issue in &orphaned {
|
||||
let rows = tx.execute(
|
||||
"DELETE FROM global_slugs WHERE slug = ?1",
|
||||
rusqlite::params![issue.slug],
|
||||
)?;
|
||||
removed_count += rows;
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
if slug_filter.is_some() {
|
||||
println!("Removed:\n\nSUCCESS");
|
||||
} else {
|
||||
println!("Repair Complete\n");
|
||||
println!("Removed:\n");
|
||||
println!("Pages:\n {}\n", orphaned_pages);
|
||||
println!("URLs:\n {}\n", orphaned_urls);
|
||||
|
||||
let remaining: i64 =
|
||||
sys_conn
|
||||
.query_row("SELECT COUNT(*) FROM global_slugs;", [], |r| r.get(0))?;
|
||||
println!("Remaining Registry Entries:\n {}\n", remaining);
|
||||
|
||||
let post_issues =
|
||||
RegistryValidator::scan(&sys_conn, &usr_conn, &config.data_dir, None)?;
|
||||
let post_orphaned = post_issues
|
||||
.iter()
|
||||
.filter(|i| {
|
||||
matches!(
|
||||
i.issue_type,
|
||||
RegistryIssueType::MissingTarget
|
||||
| RegistryIssueType::MissingDatabase
|
||||
| RegistryIssueType::MissingOwner
|
||||
)
|
||||
})
|
||||
.count();
|
||||
|
||||
println!(
|
||||
"Integrity:\n {}",
|
||||
if post_orphaned == 0 { "PASS" } else { "FAIL" }
|
||||
);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Registry Repair Started. Scanned. Orphaned Pages: {}, Orphaned URLs: {}. Removed: {}. Duration: {:?}",
|
||||
orphaned_pages, orphaned_urls, removed_count, start_time.elapsed()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+9
-4
@@ -60,15 +60,20 @@ pub fn perform_restore(
|
||||
if system_db_path.exists() && users_db_path.exists() {
|
||||
let system_conn = rusqlite::Connection::open(&system_db_path)?;
|
||||
let users_conn = rusqlite::Connection::open(&users_db_path)?;
|
||||
match crate::db::users::verify_global_slug_registry_integrity(
|
||||
match crate::services::registry_validator::RegistryValidator::scan(
|
||||
&system_conn,
|
||||
&users_conn,
|
||||
&temp_dir,
|
||||
None,
|
||||
) {
|
||||
Ok((errors, _warnings)) => {
|
||||
if !errors.is_empty() {
|
||||
Ok(issues) => {
|
||||
if !issues.is_empty() {
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
return Err(format!("Registry integrity errors in backup: {:?}", errors).into());
|
||||
return Err(format!(
|
||||
"Registry integrity errors in backup: {} issues detected",
|
||||
issues.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in new issue
Block a user