use anyhow::Result;
use rusqlite::Connection;
use std::collections::{HashMap, HashSet};
use crate::cache;
use crate::config::Config;
use crate::db;
use crate::storage;
use crate::ui;
pub fn run(conn: &Connection, cfg: &Config, assume_yes: bool) -> Result<()> {
storage::check_ready(cfg)?;
let stored = storage::list(cfg)?;
if stored.is_empty() {
println!("nothing stored at {}", cfg.remote);
return Ok(());
}
let live: HashSet<String> = db::all(conn)?
.into_iter()
.map(|doc| doc.remote_path)
.collect();
let known: HashMap<String, db::Tombstone> = db::tombstones(conn)?
.into_iter()
.map(|t| (t.remote_path.clone(), t))
.collect();
let present: HashSet<&str> = stored.iter().map(|(path, _)| path.as_str()).collect();
clear_stale_tombstones(conn, &known, &live, &present)?;
let orphans: Vec<(String, u64)> = stored
.iter()
.filter(|(path, _)| !live.contains(path))
.cloned()
.collect();
if orphans.is_empty() {
println!(
"no orphans: every stored file is referenced by the catalog ({} document(s)).",
live.len()
);
return Ok(());
}
let total: u64 = orphans.iter().map(|(_, size)| size).sum();
println!("orphaned files at {}:\n", cfg.remote);
for (path, size) in &orphans {
match known.get(path) {
Some(t) => println!(
" {} {} {} deleted {}",
ui::truncate(&t.title, 40),
ui::truncate(t.authors.as_deref().unwrap_or("-"), 20),
cache::human_bytes(*size),
&t.deleted_at[..10.min(t.deleted_at.len())]
),
None => println!(
" {} {} unknown origin",
ui::truncate(path, 62),
cache::human_bytes(*size)
),
}
}
println!(
"\n{} file(s), {} — permanently deleted from {}",
orphans.len(),
cache::human_bytes(total),
cfg.remote
);
if !assume_yes && !ui::confirm("\ndelete these files?")? {
println!("cancelled.");
return Ok(());
}
let mut deleted = 0;
let mut freed = 0;
for (path, size) in &orphans {
match storage::delete(cfg, path) {
Ok(()) => {
if let Some(t) = known.get(path) {
db::forget_tombstone(conn, &t.content_hash)?;
}
deleted += 1;
freed += size;
}
Err(e) => eprintln!(" failed to delete {path}: {e:#}"),
}
}
println!(
"deleted {deleted} file(s), freed {}",
cache::human_bytes(freed)
);
Ok(())
}
fn clear_stale_tombstones(
conn: &Connection,
known: &HashMap<String, db::Tombstone>,
live: &HashSet<String>,
present: &HashSet<&str>,
) -> Result<()> {
for (path, t) in known {
if live.contains(path) || !present.contains(path.as_str()) {
db::forget_tombstone(conn, &t.content_hash)?;
}
}
Ok(())
}