ipfrs_cli/commands/
repo.rs1use anyhow::Result;
11
12use crate::output::{self, error, format_bytes, print_header, print_kv, success};
13use crate::progress;
14
15use super::stats::stats_repo;
16
17pub async fn repo_gc(dry_run: bool, min_age_secs: u64, format: &str) -> Result<()> {
21 use ipfrs::{Node, NodeConfig};
22
23 let action = if dry_run { "Analyzing" } else { "Running" };
24 let pb = progress::spinner(&format!("{} garbage collection", action));
25 let mut node = Node::new(NodeConfig::default())?;
26 node.start().await?;
27
28 let result = node.repo_gc_with_options(dry_run, min_age_secs).await?;
29 progress::finish_spinner_success(&pb, "GC complete");
30
31 match format {
32 "json" => {
33 println!("{{");
34 println!(" \"blocks_collected\": {},", result.blocks_collected);
35 println!(" \"bytes_freed\": {},", result.bytes_freed);
36 println!(" \"blocks_marked\": {},", result.blocks_marked);
37 println!(" \"blocks_scanned\": {},", result.blocks_scanned);
38 println!(" \"duration_ms\": {}", result.duration.as_millis());
39 println!("}}");
40 }
41 _ => {
42 print_header("Garbage Collection Results");
43 print_kv("Blocks scanned", &result.blocks_scanned.to_string());
44 print_kv("Blocks marked", &result.blocks_marked.to_string());
45 print_kv("Blocks collected", &result.blocks_collected.to_string());
46 print_kv("Bytes freed", &format_bytes(result.bytes_freed));
47 print_kv(
48 "Duration",
49 &format!("{:.2}s", result.duration.as_secs_f64()),
50 );
51
52 if dry_run {
53 output::warning("Dry run - no blocks were actually deleted");
54 } else if result.blocks_collected > 0 {
55 success(&format!(
56 "Freed {} from {} blocks",
57 format_bytes(result.bytes_freed),
58 result.blocks_collected
59 ));
60 } else {
61 output::info("No unreferenced blocks found");
62 }
63 }
64 }
65
66 node.stop().await?;
67 Ok(())
68}
69
70pub async fn repo_stat(format: &str) -> Result<()> {
72 stats_repo(format).await
74}
75
76pub async fn repo_fsck(format: &str) -> Result<()> {
78 use ipfrs::{Node, NodeConfig};
79
80 let pb = progress::spinner("Checking repository integrity");
81 let mut node = Node::new(NodeConfig::default())?;
82 node.start().await?;
83
84 let result = node.repo_fsck().await?;
85 progress::finish_spinner_success(&pb, "Integrity check complete");
86
87 match format {
88 "json" => {
89 println!("{{");
90 println!(" \"blocks_checked\": {},", result.blocks_checked);
91 println!(" \"blocks_valid\": {},", result.blocks_valid);
92 println!(" \"blocks_corrupt\": {},", result.blocks_corrupt.len());
93 println!(" \"blocks_missing\": {}", result.blocks_missing.len());
94 println!("}}");
95 }
96 _ => {
97 print_header("Repository Integrity Check");
98 print_kv("Blocks checked", &result.blocks_checked.to_string());
99 print_kv("Valid blocks", &result.blocks_valid.to_string());
100 print_kv("Corrupt blocks", &result.blocks_corrupt.len().to_string());
101 print_kv("Missing blocks", &result.blocks_missing.len().to_string());
102
103 if result.blocks_corrupt.is_empty() && result.blocks_missing.is_empty() {
104 success("Repository integrity verified - no issues found");
105 } else {
106 if !result.blocks_corrupt.is_empty() {
107 println!("\nCorrupt blocks:");
108 for cid in &result.blocks_corrupt {
109 error(&format!(" {}", cid));
110 }
111 }
112 if !result.blocks_missing.is_empty() {
113 println!("\nMissing blocks:");
114 for cid in &result.blocks_missing {
115 error(&format!(" {}", cid));
116 }
117 }
118 }
119 }
120 }
121
122 node.stop().await?;
123 Ok(())
124}
125
126pub async fn repo_version(_format: &str) -> Result<()> {
128 print_header("Repository Version");
129 print_kv("Format version", "1");
130 print_kv("IPFRS version", env!("CARGO_PKG_VERSION"));
131 Ok(())
132}
133
134pub async fn storage_compact(force: bool, format: &str) -> Result<()> {
144 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
145 use std::time::Instant;
146
147 let pb = progress::spinner(if force {
148 "Forcing WAL flush / compaction"
149 } else {
150 "Checking compaction schedule"
151 });
152
153 let config = BlockStoreConfig::default();
154 let store = SledBlockStore::new(config)?;
155
156 let start = Instant::now();
157
158 let (triggered, reason) = if force {
159 let triggered = store.maybe_compact().await?;
174 (
178 true,
179 if triggered {
180 "forced (scheduler agreed)"
181 } else {
182 "forced (scheduler skipped)"
183 },
184 )
185 } else {
186 let triggered = store.maybe_compact().await?;
187 let reason = if triggered { "scheduled" } else { "not_due" };
188 (triggered, reason)
189 };
190
191 let duration_ms = start.elapsed().as_millis() as u64;
192
193 let dedup = store.dedup_stats().snapshot();
195
196 progress::finish_spinner_success(
197 &pb,
198 if triggered {
199 "Compaction complete"
200 } else {
201 "No compaction needed"
202 },
203 );
204
205 match format {
206 "json" => {
207 println!("{{");
208 println!(" \"triggered\": {},", triggered);
209 println!(" \"reason\": \"{}\",", reason);
210 println!(" \"duration_ms\": {},", duration_ms);
211 println!(" \"dedup_attempts\": {},", dedup.total_puts);
212 println!(" \"dedup_count\": {},", dedup.deduplicated);
213 println!(" \"dedup_bytes_saved\": {},", dedup.bytes_saved);
214 println!(" \"dedup_rate\": {:.4}", dedup.dedup_ratio);
215 println!("}}");
216 }
217 _ => {
218 print_header("Storage Compaction");
219 print_kv("Triggered", &triggered.to_string());
220 print_kv("Reason", reason);
221 print_kv("Duration", &format!("{} ms", duration_ms));
222
223 println!();
224 print_header("Deduplication Statistics");
225 print_kv("Total write attempts", &dedup.total_puts.to_string());
226 print_kv("Deduplicated writes", &dedup.deduplicated.to_string());
227 print_kv("Bytes saved", &format_bytes(dedup.bytes_saved));
228 print_kv("Dedup rate", &format!("{:.1}%", dedup.dedup_ratio * 100.0));
229
230 if triggered {
231 success("WAL flush completed successfully");
232 } else {
233 output::info("Compaction not due yet — run with --force to flush immediately");
234 }
235 }
236 }
237
238 Ok(())
239}
240
241#[cfg(test)]
246mod compact_tests {
247 #[test]
250 fn test_offline_error_message_format() {
251 let msg = crate::connectivity::offline_error_message("/tmp/test-repo");
252 assert!(
253 msg.contains("daemon is not running"),
254 "message should mention daemon not running"
255 );
256 assert!(
257 msg.contains("/tmp/test-repo"),
258 "message should embed the data_dir path"
259 );
260 }
261}