use super::daemon_utils::daemon_base_url;
use anyhow::{bail, Result};
use colored::Colorize;
use std::io::{BufRead, Write};
struct EmptyIndex {
id: String,
root_path: String,
}
pub async fn handle_cleanup(yes: bool, dry_run: bool) -> Result<()> {
let base = daemon_base_url();
let client = trusty_common::server::daemon_http_client()?;
let list_url = format!("{}/indexes", base);
let list_body: serde_json::Value = match client.get(&list_url).send().await {
Ok(resp) if resp.status().is_success() => resp
.json()
.await
.unwrap_or_else(|_| serde_json::json!({"indexes": []})),
Ok(resp) => bail!("daemon returned {} for {}", resp.status(), list_url),
Err(e) => bail!("could not reach daemon at {}: {e}", base),
};
let empty_arr: Vec<serde_json::Value> = Vec::new();
let ids: Vec<String> = list_body
.get("indexes")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr)
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
let mut joinset = tokio::task::JoinSet::new();
for id in &ids {
let n = id.clone();
let url = format!("{}/indexes/{}/status", base, n);
let c = client.clone();
joinset.spawn(async move {
let body: serde_json::Value = match c.get(&url).send().await {
Ok(r) if r.status().is_success() => {
r.json().await.unwrap_or_else(|_| serde_json::json!({}))
}
_ => serde_json::json!({}),
};
(n, body)
});
}
let mut empties: Vec<EmptyIndex> = Vec::new();
while let Some(j) = joinset.join_next().await {
if let Ok((id, body)) = j {
let chunks = body
.get("chunk_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
if chunks == 0 {
let root_path = body
.get("root_path")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
empties.push(EmptyIndex { id, root_path });
}
}
}
empties.sort_by(|a, b| a.id.cmp(&b.id));
if empties.is_empty() {
println!("Nothing to clean up.");
return Ok(());
}
let count = empties.len();
println!(
"{} {} empty indexes (0 chunks):",
"Found".bold(),
count.to_string().bold()
);
let name_width = empties.iter().map(|e| e.id.len()).max().unwrap_or(0).max(4);
for e in &empties {
if e.root_path.is_empty() {
println!(" {:<width$}", e.id.bold(), width = name_width);
} else {
println!(
" {:<width$} {}",
e.id.bold(),
e.root_path.dimmed(),
width = name_width
);
}
}
if dry_run {
println!("{} dry-run: no indexes were removed.", "ℹ".cyan());
return Ok(());
}
if !yes && !confirm(&format!("Remove these {} indexes?", count))? {
println!("Aborted.");
return Ok(());
}
let mut removed = 0usize;
let mut failed: Vec<(String, String)> = Vec::new();
for e in &empties {
let url = format!("{}/indexes/{}", base, e.id);
match client.delete(&url).send().await {
Ok(resp) if resp.status().is_success() => {
removed += 1;
}
Ok(resp) => {
failed.push((e.id.clone(), format!("HTTP {}", resp.status())));
}
Err(err) => {
failed.push((e.id.clone(), err.to_string()));
}
}
}
if failed.is_empty() {
println!(
"{} Removed {} empty indexes.",
"✓".green(),
removed.to_string().bold()
);
} else {
println!(
"{} Removed {} of {} empty indexes ({} failed):",
"!".yellow(),
removed,
count,
failed.len()
);
for (id, err) in &failed {
println!(" {} {} — {}", "✗".red(), id, err.dimmed());
}
bail!("{} index removals failed", failed.len());
}
Ok(())
}
fn confirm(prompt: &str) -> Result<bool> {
print!("{} [y/N] ", prompt);
std::io::stdout().flush().ok();
let stdin = std::io::stdin();
let mut line = String::new();
stdin.lock().read_line(&mut line)?;
let answer = line.trim();
Ok(matches!(answer.chars().next(), Some('y') | Some('Y')))
}