pub mod classify;
#[cfg(test)]
mod handler_tests;
pub mod migrate;
use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
use crate::service::colocated_storage::COLOCATED_DIR_NAME;
use crate::service::persistence::{load_index_registry, save_index_registry};
use classify::{classify_index, IndexMigrationClass};
use migrate::{do_migrate_with_pointer_removal, move_data_files, try_remove_empty_src_dir};
#[derive(Debug, PartialEq, Eq)]
pub enum MigrateStorageStatus {
Migrated,
AlreadyColocated,
RootMissing,
NoData,
Failed(String),
}
#[derive(Debug)]
pub struct MigrateStorageResult {
pub id: String,
pub root_path: PathBuf,
pub status: MigrateStorageStatus,
}
pub fn handle_migrate_storage(dry_run: bool) -> Result<()> {
let mut entries = match load_index_registry() {
Ok(e) => e,
Err(e) => anyhow::bail!("could not read indexes.toml: {e}"),
};
if entries.is_empty() {
println!(
"{} No indexes registered — nothing to migrate.",
"·".dimmed()
);
return Ok(());
}
if dry_run {
println!(
"{} Dry run — no files or registry entries will be modified.\n",
"·".dimmed()
);
}
let total = entries.len();
let mut migrated_count = 0usize;
let mut already_count = 0usize;
let mut skipped_count = 0usize;
let mut failed_count = 0usize;
for entry in &mut entries {
let id = entry.id.clone();
let root = entry.root_path.clone();
let class = classify_index(&id, &root);
let result = match class {
IndexMigrationClass::AlreadyColocated => MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::AlreadyColocated,
},
IndexMigrationClass::SkipDeadRoot => MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::RootMissing,
},
IndexMigrationClass::SkipNoData => MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::NoData,
},
IndexMigrationClass::NeedsMigration { src_dir, dst_dir } => {
if dry_run {
MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::Migrated,
}
} else {
match move_data_files(&src_dir, &dst_dir, &root) {
Ok(_moved) => {
try_remove_empty_src_dir(&src_dir);
entry.colocated = true;
MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::Migrated,
}
}
Err(e) => MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::Failed(format!("{e:#}")),
},
}
}
}
IndexMigrationClass::LegacyPointerFile {
pointer_path,
src_dir,
dst_dir,
} => {
if dry_run {
MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::Migrated,
}
} else {
match do_migrate_with_pointer_removal(&pointer_path, &src_dir, &dst_dir, &root)
{
Ok(_moved) => {
try_remove_empty_src_dir(&src_dir);
entry.colocated = true;
MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::Migrated,
}
}
Err(e) => MigrateStorageResult {
id: id.clone(),
root_path: root.clone(),
status: MigrateStorageStatus::Failed(format!("{e:#}")),
},
}
}
}
};
print_migrate_line(
migrated_count + already_count + skipped_count + failed_count + 1,
total,
&result,
dry_run,
);
match result.status {
MigrateStorageStatus::Migrated => migrated_count += 1,
MigrateStorageStatus::AlreadyColocated => already_count += 1,
MigrateStorageStatus::RootMissing | MigrateStorageStatus::NoData => skipped_count += 1,
MigrateStorageStatus::Failed(_) => failed_count += 1,
}
}
if !dry_run && migrated_count > 0 {
if let Err(e) = save_index_registry(&entries) {
eprintln!(
"{} could not save indexes.toml after migration: {e:#}",
"✗".red()
);
} else {
tracing::info!(
"migrate storage: updated indexes.toml — {} entries now colocated",
migrated_count
);
}
}
println!();
if dry_run {
println!(
"{} Dry run: {} would migrate, {} already colocated, {} skipped, {} failed",
"·".dimmed(),
migrated_count,
already_count,
skipped_count,
failed_count
);
} else {
println!(
"{} Migrate storage: {} migrated, {} already colocated, {} skipped, {} failed",
if failed_count == 0 {
"✓".green()
} else {
"⚠".yellow()
},
migrated_count,
already_count,
skipped_count,
failed_count
);
if migrated_count > 0 {
println!(
"\n{} Restart the daemon to use the new colocated paths:\n trusty-search stop && trusty-search start",
"ℹ".cyan()
);
}
}
Ok(())
}
fn print_migrate_line(idx: usize, total: usize, r: &MigrateStorageResult, dry_run: bool) {
let prefix = format!("[{idx}/{total}]");
let id = &r.id;
let path = r.root_path.display().to_string();
match &r.status {
MigrateStorageStatus::Migrated => {
if dry_run {
println!(
" {} {} {} {}",
prefix.dimmed(),
"→".cyan(),
id.bold(),
format!("(would move to {}/.trusty-search/)", path).dimmed()
);
} else {
println!(
" {} {} {} → {}{}",
prefix.dimmed(),
"✓".green(),
id.bold(),
path.dimmed(),
format!("/{COLOCATED_DIR_NAME}/").dimmed()
);
}
}
MigrateStorageStatus::AlreadyColocated => println!(
" {} {} {} {}",
prefix.dimmed(),
"↻".cyan(),
id.dimmed(),
"(already colocated — filesystem confirmed)".dimmed()
),
MigrateStorageStatus::RootMissing => println!(
" {} {} {} {}",
prefix.dimmed(),
"·".dimmed(),
id.dimmed(),
format!("(root_path missing: {path})").dimmed()
),
MigrateStorageStatus::NoData => println!(
" {} {} {} {}",
prefix.dimmed(),
"·".dimmed(),
id.dimmed(),
"(no data in app-data or colocated dir — skipped)".dimmed()
),
MigrateStorageStatus::Failed(msg) => println!(
" {} {} {} {}",
prefix.dimmed(),
"✗".red(),
id.dimmed(),
format!("({msg})").red()
),
}
}