forked from
atscan.net/plcbundle-rs
High-performance implementation of plcbundle written in Rust
1use super::utils;
2use anyhow::Result;
3use clap::Args;
4use plcbundle::CleanPreview;
5use std::io::{self, Write};
6use std::path::PathBuf;
7
8#[derive(Args)]
9#[command(
10 about = "Remove all temporary files from the repository",
11 alias = "cleanup",
12 long_about = "Remove temporary files created during atomic write operations. These files
13have .tmp extensions and are used to ensure data integrity when writing index
14files, DID index shards, and other critical repository data.
15
16Temporary files should normally be cleaned up automatically when operations
17complete successfully. However, if a process is interrupted (e.g., by Ctrl+C
18or a crash), temporary files may remain on disk. This command safely removes
19all such files after showing you what will be deleted.
20
21This is a safe operation that only affects temporary files. Your actual bundle
22data and index files are never touched. Use this command periodically or after
23interrupted operations to keep your repository clean.",
24 help_template = crate::clap_help!(
25 examples: " # Clean all temporary files (with confirmation)\n \
26 {bin} clean\n\n \
27 # Clean without confirmation prompt\n \
28 {bin} clean --force\n\n \
29 # Clean with verbose output\n \
30 {bin} clean --verbose\n\n \
31 # Using alias\n \
32 {bin} cleanup"
33 )
34)]
35pub struct CleanCommand {
36 /// Show verbose output
37 #[arg(short, long)]
38 pub verbose: bool,
39
40 /// Skip confirmation prompt
41 #[arg(short, long)]
42 pub force: bool,
43}
44
45pub fn run(cmd: CleanCommand, dir: PathBuf, global_verbose: bool) -> Result<()> {
46 let verbose = cmd.verbose || global_verbose;
47 let manager = utils::create_manager(dir.clone(), verbose, false, false)?;
48
49 // Step 1: Preview what will be cleaned
50 let preview = manager.clean_preview()?;
51
52 // Step 2: Display preview
53 display_clean_preview(&dir, &preview, verbose)?;
54
55 // Step 3: Ask for confirmation (unless --force)
56 if preview.files.is_empty() {
57 println!("✓ No temporary files found - repository is clean");
58 return Ok(());
59 }
60
61 if !cmd.force {
62 if !confirm_clean()? {
63 println!("Cancelled");
64 return Ok(());
65 }
66 println!();
67 }
68
69 // Step 4: Perform cleanup
70 if verbose {
71 println!("Cleaning temporary files from repository...");
72 println!("Directory: {}\n", utils::display_path(&dir).display());
73 }
74
75 let result = manager.clean()?;
76
77 // Step 5: Display results
78 println!("✓ Cleaned {} temporary file(s)", result.files_removed);
79 if result.bytes_freed > 0 {
80 println!(" Freed: {}", utils::format_bytes(result.bytes_freed));
81 }
82
83 // Show errors if any
84 if let Some(errors) = &result.errors
85 && !errors.is_empty()
86 {
87 eprintln!("\n⚠ Warning: Some errors occurred during cleanup:");
88 for error in errors {
89 eprintln!(" - {}", error);
90 }
91 }
92
93 Ok(())
94}
95
96fn display_clean_preview(dir: &PathBuf, preview: &CleanPreview, _verbose: bool) -> Result<()> {
97 println!("Files to be deleted:");
98 println!();
99
100 if preview.files.is_empty() {
101 return Ok(());
102 }
103
104 // Group files by directory for better display
105 let mut root_files = Vec::new();
106 let mut config_files = Vec::new();
107 let mut shard_files = Vec::new();
108
109 for file in &preview.files {
110 let path_str = file.path.to_string_lossy();
111 if path_str.contains(".plcbundle/shards/") {
112 shard_files.push(file);
113 } else if path_str.contains(".plcbundle/") {
114 config_files.push(file);
115 } else {
116 root_files.push(file);
117 }
118 }
119
120 // Display root files
121 if !root_files.is_empty() {
122 println!(" Repository root:");
123 for file in &root_files {
124 let rel_path = file
125 .path
126 .strip_prefix(dir)
127 .unwrap_or(&file.path)
128 .to_string_lossy();
129 println!(" • {} ({})", rel_path, utils::format_bytes(file.size));
130 }
131 println!();
132 }
133
134 // Display config files
135 if !config_files.is_empty() {
136 println!(" DID index directory:");
137 for file in &config_files {
138 let rel_path = file
139 .path
140 .strip_prefix(dir)
141 .unwrap_or(&file.path)
142 .to_string_lossy();
143 println!(" • {} ({})", rel_path, utils::format_bytes(file.size));
144 }
145 println!();
146 }
147
148 // Display shard files
149 if !shard_files.is_empty() {
150 println!(" Shards directory:");
151 for file in &shard_files {
152 let rel_path = file
153 .path
154 .strip_prefix(dir)
155 .unwrap_or(&file.path)
156 .to_string_lossy();
157 println!(" • {} ({})", rel_path, utils::format_bytes(file.size));
158 }
159 println!();
160 }
161
162 println!(
163 " Total: {} file(s), {}",
164 preview.files.len(),
165 utils::format_bytes(preview.total_size)
166 );
167 println!();
168
169 Ok(())
170}
171
172fn confirm_clean() -> Result<bool> {
173 print!("Delete these files? [y/N]: ");
174 io::stdout().flush()?;
175
176 let mut response = String::new();
177 io::stdin().read_line(&mut response)?;
178
179 let response = response.trim().to_lowercase();
180 Ok(response == "y" || response == "yes")
181}