Skip to content

Commit c68f19b

Browse files
committed
feat: add config cleanup subcommand
Signed-off-by: Luca Ferrazzini <luca733@gmail.com>
1 parent d2d72bb commit c68f19b

3 files changed

Lines changed: 243 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ tracing = { version = "0.1.41", default-features = false, features = ["attribute
5656
tracing-subscriber = { version = "0.3.19", default-features = false, features = ["env-filter", "ansi", "time", "json"] }
5757
url = { version = "2.5", default-features = false }
5858
uuid = { version = "1.17.0", default-features = false, features = ["std"] }
59+
walkdir = {version = "2.5.0", default-features = false }
5960
wasm-pkg-client = { version = "0.10.0", default-features = false }
6061
wasm-pkg-core = { version = "0.10.0", default-features = false }
6162
wasmcloud-runtime = { git = "https://github.com/wasmCloud/wasmCloud", version = "0.11.0" }

crates/wash/src/cli/config.rs

Lines changed: 241 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,27 @@ use anyhow::Context as _;
22
use clap::Subcommand;
33
use etcetera::AppStrategy as _;
44
use tracing::instrument;
5+
use tracing::{error, info, warn};
6+
use walkdir::WalkDir;
57

68
use crate::{
79
cli::{CliCommand, CliContext, CommandOutput},
810
config::{generate_default_config, local_config_path},
911
};
12+
use std::io::{self};
13+
use std::path::{Path, PathBuf};
1014

15+
#[instrument(skip(dir), fields(path = %dir.display()))]
16+
fn get_all_paths_in_dir(dir: &Path) -> io::Result<Vec<PathBuf>> {
17+
let paths: Vec<PathBuf> = WalkDir::new(dir)
18+
.into_iter()
19+
.filter_map(|entry| entry.ok())
20+
.filter(|entry| entry.path() != dir)
21+
.map(|entry| entry.into_path())
22+
.collect();
23+
24+
Ok(paths)
25+
}
1126
/// Create a new component project from a template, git repository, or local path
1227
#[derive(Subcommand, Debug, Clone)]
1328
pub enum ConfigCommand {
@@ -25,7 +40,27 @@ pub enum ConfigCommand {
2540
/// Print the current configuration file for wash
2641
Show {},
2742
// TODO(#27): validate config command
28-
// TODO(#29): cleanup config command, to clean the dirs we use
43+
/// Clean up wash directories and cached data
44+
#[clap(group = clap::ArgGroup::new("cleanup_targets")
45+
.required(true)
46+
.multiple(true))]
47+
Cleanup {
48+
/// Remove config directory
49+
#[clap(long, group = "cleanup_targets")]
50+
config: bool,
51+
/// Remove cache directory
52+
#[clap(long, group = "cleanup_targets")]
53+
cache: bool,
54+
/// Remove data directory
55+
#[clap(long, group = "cleanup_targets")]
56+
data: bool,
57+
/// Remove all wash directories (config + cache + data)
58+
#[clap(long, group = "cleanup_targets")]
59+
all: bool,
60+
/// Show what would be removed without actually deleting
61+
#[clap(long)]
62+
dry_run: bool,
63+
},
2964
}
3065

3166
impl CliCommand for ConfigCommand {
@@ -84,6 +119,211 @@ impl CliCommand for ConfigCommand {
84119
Some(serde_json::to_value(&config).context("failed to serialize config")?),
85120
))
86121
}
122+
ConfigCommand::Cleanup {
123+
config,
124+
cache,
125+
data,
126+
all,
127+
dry_run,
128+
} => {
129+
let config_dir = ctx.config_dir();
130+
let cache_dir = ctx.cache_dir();
131+
let data_dir = ctx.data_dir();
132+
133+
let mut cleanup_paths: Vec<PathBuf> = Vec::new();
134+
135+
if *config || *all {
136+
let config_paths: Vec<PathBuf> = get_all_paths_in_dir(config_dir.as_path())?;
137+
cleanup_paths.extend(config_paths);
138+
}
139+
140+
if *cache || *all {
141+
let cache_paths: Vec<PathBuf> = get_all_paths_in_dir(cache_dir.as_path())?;
142+
cleanup_paths.extend(cache_paths);
143+
}
144+
145+
if *data || *all {
146+
let data_paths: Vec<PathBuf> = get_all_paths_in_dir(data_dir.as_path())?;
147+
cleanup_paths.extend(data_paths);
148+
}
149+
150+
let cleanup_files: Vec<PathBuf> = cleanup_paths
151+
.iter()
152+
.filter(|p| p.is_file())
153+
.cloned()
154+
.collect();
155+
156+
let mut cleanup_dirs: Vec<PathBuf> = cleanup_paths
157+
.iter()
158+
.filter(|p| p.is_dir())
159+
.cloned()
160+
.collect();
161+
162+
// Sort to first delete the deepest directories
163+
cleanup_dirs.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
164+
165+
// Gather all files as a string for output
166+
let files_summary = cleanup_paths
167+
.iter()
168+
.filter(|p| p.is_file())
169+
.map(|p| p.display().to_string())
170+
.collect::<Vec<String>>()
171+
.join("\n");
172+
173+
if cleanup_files.is_empty() {
174+
return Ok(CommandOutput::ok(
175+
"No files were found to clean up.",
176+
Some(serde_json::json!({
177+
"message": "No files were found to clean up.",
178+
"success": true,
179+
})),
180+
));
181+
}
182+
183+
if *dry_run {
184+
return Ok(CommandOutput::ok(
185+
format!(
186+
"Found {} files for cleanup (Dry Run):\n{}\n\n",
187+
cleanup_files.len(),
188+
files_summary
189+
),
190+
Some(serde_json::json!({
191+
"message": "Dry run executed successfully. No files were deleted.",
192+
"success": true,
193+
"file_count": cleanup_files.len(),
194+
"files": files_summary
195+
})),
196+
));
197+
}
198+
199+
warn!(
200+
"Found {} files for cleanup. Files to be deleted:\n{}\n\nDo you want to proceed with the deletion? (y/N)",
201+
cleanup_files.len(),
202+
files_summary
203+
);
204+
205+
let mut successful_deletions = 0;
206+
let mut failed_paths = Vec::new();
207+
208+
let mut confirmation = String::new();
209+
io::stdin().read_line(&mut confirmation)?;
210+
211+
if !confirmation.trim().eq_ignore_ascii_case("y") {
212+
return Ok(CommandOutput::ok(
213+
format!("Skipped deletion of {} files", cleanup_files.len()),
214+
Some(serde_json::json!({
215+
"message": "File deletion skipped.",
216+
"success": true,
217+
})),
218+
));
219+
}
220+
221+
for path in &cleanup_files {
222+
match std::fs::remove_file(path) {
223+
Ok(_) => {
224+
info!("Successfully deleted file: {:?}", path.display());
225+
successful_deletions += 1
226+
}
227+
Err(e) => {
228+
error!("Failed to delete {} file: {}", path.display(), e);
229+
failed_paths.push(path.clone());
230+
}
231+
}
232+
}
233+
234+
for path in &cleanup_dirs {
235+
match std::fs::remove_dir_all(path) {
236+
Ok(_) => {
237+
info!("Successfully deleted dir: {:?}", path.display());
238+
}
239+
Err(e) => {
240+
error!("Failed to delete dir {}: {}", path.display(), e);
241+
failed_paths.push(path.clone());
242+
}
243+
}
244+
}
245+
246+
if !failed_paths.is_empty() {
247+
return Ok(CommandOutput::error(
248+
format!("Failed to delete {} files", failed_paths.len()),
249+
Some(serde_json::json!({
250+
"message": format!("Partial failure: Deleted {}/{} files.",
251+
successful_deletions,
252+
cleanup_files.len()),
253+
"deleted": successful_deletions,
254+
"failed_count": failed_paths.len(),
255+
"success": false,
256+
})),
257+
));
258+
}
259+
260+
return Ok(CommandOutput::ok(
261+
format!("Successfully deleted {successful_deletions} files"),
262+
Some(serde_json::json!({
263+
"message": format!("{successful_deletions} files deleted successfully."),
264+
"deleted": successful_deletions,
265+
})),
266+
));
267+
}
87268
}
88269
}
89270
}
271+
272+
#[cfg(test)]
273+
mod tests {
274+
use super::*;
275+
use tempfile::TempDir;
276+
277+
#[test]
278+
fn test_get_all_paths_in_dir() {
279+
let temp_dir = TempDir::new().expect("failed to create temp dir");
280+
let root_path = temp_dir.path();
281+
282+
let file1_path = root_path.join("file1.txt");
283+
let subdir_path = root_path.join("subdir");
284+
let file2_path = subdir_path.join("file2.log");
285+
let emptydir_path = root_path.join("empty");
286+
287+
// Create the files and directories
288+
std::fs::write(&file1_path, "content")
289+
.expect(&format!("failed to create file {}", file1_path.display()));
290+
std::fs::create_dir(&subdir_path).expect(&format!(
291+
"failed to create directory {}",
292+
subdir_path.display()
293+
));
294+
std::fs::write(&file2_path, "more content")
295+
.expect(&format!("failed to create file {}", file2_path.display()));
296+
std::fs::create_dir(&emptydir_path).expect(&format!(
297+
"failed to create directory {}",
298+
emptydir_path.display()
299+
));
300+
301+
let mut actual_paths = get_all_paths_in_dir(root_path).expect(&format!(
302+
"failed to get files from root path {}",
303+
root_path.display()
304+
));
305+
306+
let mut expected_files = vec![
307+
file1_path.to_path_buf(),
308+
subdir_path.to_path_buf(),
309+
file2_path.to_path_buf(),
310+
emptydir_path.to_path_buf(),
311+
];
312+
313+
// Sort to ensure the order of results doesn't cause the test to fail
314+
actual_paths.sort();
315+
expected_files.sort();
316+
317+
assert_eq!(
318+
actual_paths, expected_files,
319+
"Actual files and expected files do not match."
320+
);
321+
322+
// Explicitly check the count for clarity
323+
assert_eq!(
324+
actual_paths.len(),
325+
4,
326+
"Should have found exactly two files."
327+
);
328+
}
329+
}

0 commit comments

Comments
 (0)