use anyhow::Result;
use crate::output::{self, error, format_bytes, print_header, print_kv, success};
use crate::progress;
use super::stats::stats_repo;
pub async fn repo_gc(dry_run: bool, min_age_secs: u64, format: &str) -> Result<()> {
use ipfrs::{Node, NodeConfig};
let action = if dry_run { "Analyzing" } else { "Running" };
let pb = progress::spinner(&format!("{} garbage collection", action));
let mut node = Node::new(NodeConfig::default())?;
node.start().await?;
let result = node.repo_gc_with_options(dry_run, min_age_secs).await?;
progress::finish_spinner_success(&pb, "GC complete");
match format {
"json" => {
println!("{{");
println!(" \"blocks_collected\": {},", result.blocks_collected);
println!(" \"bytes_freed\": {},", result.bytes_freed);
println!(" \"blocks_marked\": {},", result.blocks_marked);
println!(" \"blocks_scanned\": {},", result.blocks_scanned);
println!(" \"duration_ms\": {}", result.duration.as_millis());
println!("}}");
}
_ => {
print_header("Garbage Collection Results");
print_kv("Blocks scanned", &result.blocks_scanned.to_string());
print_kv("Blocks marked", &result.blocks_marked.to_string());
print_kv("Blocks collected", &result.blocks_collected.to_string());
print_kv("Bytes freed", &format_bytes(result.bytes_freed));
print_kv(
"Duration",
&format!("{:.2}s", result.duration.as_secs_f64()),
);
if dry_run {
output::warning("Dry run - no blocks were actually deleted");
} else if result.blocks_collected > 0 {
success(&format!(
"Freed {} from {} blocks",
format_bytes(result.bytes_freed),
result.blocks_collected
));
} else {
output::info("No unreferenced blocks found");
}
}
}
node.stop().await?;
Ok(())
}
pub async fn repo_stat(format: &str) -> Result<()> {
stats_repo(format).await
}
pub async fn repo_fsck(format: &str) -> Result<()> {
use ipfrs::{Node, NodeConfig};
let pb = progress::spinner("Checking repository integrity");
let mut node = Node::new(NodeConfig::default())?;
node.start().await?;
let result = node.repo_fsck().await?;
progress::finish_spinner_success(&pb, "Integrity check complete");
match format {
"json" => {
println!("{{");
println!(" \"blocks_checked\": {},", result.blocks_checked);
println!(" \"blocks_valid\": {},", result.blocks_valid);
println!(" \"blocks_corrupt\": {},", result.blocks_corrupt.len());
println!(" \"blocks_missing\": {}", result.blocks_missing.len());
println!("}}");
}
_ => {
print_header("Repository Integrity Check");
print_kv("Blocks checked", &result.blocks_checked.to_string());
print_kv("Valid blocks", &result.blocks_valid.to_string());
print_kv("Corrupt blocks", &result.blocks_corrupt.len().to_string());
print_kv("Missing blocks", &result.blocks_missing.len().to_string());
if result.blocks_corrupt.is_empty() && result.blocks_missing.is_empty() {
success("Repository integrity verified - no issues found");
} else {
if !result.blocks_corrupt.is_empty() {
println!("\nCorrupt blocks:");
for cid in &result.blocks_corrupt {
error(&format!(" {}", cid));
}
}
if !result.blocks_missing.is_empty() {
println!("\nMissing blocks:");
for cid in &result.blocks_missing {
error(&format!(" {}", cid));
}
}
}
}
}
node.stop().await?;
Ok(())
}
pub async fn repo_version(_format: &str) -> Result<()> {
print_header("Repository Version");
print_kv("Format version", "1");
print_kv("IPFRS version", env!("CARGO_PKG_VERSION"));
Ok(())
}
pub async fn storage_compact(force: bool, format: &str) -> Result<()> {
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
use std::time::Instant;
let pb = progress::spinner(if force {
"Forcing WAL flush / compaction"
} else {
"Checking compaction schedule"
});
let config = BlockStoreConfig::default();
let store = SledBlockStore::new(config)?;
let start = Instant::now();
let (triggered, reason) = if force {
let triggered = store.maybe_compact().await?;
(
true,
if triggered {
"forced (scheduler agreed)"
} else {
"forced (scheduler skipped)"
},
)
} else {
let triggered = store.maybe_compact().await?;
let reason = if triggered { "scheduled" } else { "not_due" };
(triggered, reason)
};
let duration_ms = start.elapsed().as_millis() as u64;
let dedup = store.dedup_stats().snapshot();
progress::finish_spinner_success(
&pb,
if triggered {
"Compaction complete"
} else {
"No compaction needed"
},
);
match format {
"json" => {
println!("{{");
println!(" \"triggered\": {},", triggered);
println!(" \"reason\": \"{}\",", reason);
println!(" \"duration_ms\": {},", duration_ms);
println!(" \"dedup_attempts\": {},", dedup.total_puts);
println!(" \"dedup_count\": {},", dedup.deduplicated);
println!(" \"dedup_bytes_saved\": {},", dedup.bytes_saved);
println!(" \"dedup_rate\": {:.4}", dedup.dedup_ratio);
println!("}}");
}
_ => {
print_header("Storage Compaction");
print_kv("Triggered", &triggered.to_string());
print_kv("Reason", reason);
print_kv("Duration", &format!("{} ms", duration_ms));
println!();
print_header("Deduplication Statistics");
print_kv("Total write attempts", &dedup.total_puts.to_string());
print_kv("Deduplicated writes", &dedup.deduplicated.to_string());
print_kv("Bytes saved", &format_bytes(dedup.bytes_saved));
print_kv("Dedup rate", &format!("{:.1}%", dedup.dedup_ratio * 100.0));
if triggered {
success("WAL flush completed successfully");
} else {
output::info("Compaction not due yet — run with --force to flush immediately");
}
}
}
Ok(())
}
#[cfg(test)]
mod compact_tests {
#[test]
fn test_offline_error_message_format() {
let msg = crate::connectivity::offline_error_message("/tmp/test-repo");
assert!(
msg.contains("daemon is not running"),
"message should mention daemon not running"
);
assert!(
msg.contains("/tmp/test-repo"),
"message should embed the data_dir path"
);
}
}