@@ -2,12 +2,27 @@ use anyhow::Context as _;
22use clap:: Subcommand ;
33use etcetera:: AppStrategy as _;
44use tracing:: instrument;
5+ use tracing:: { error, info, warn} ;
6+ use walkdir:: WalkDir ;
57
68use 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 ) ]
1328pub 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
3166impl 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 \n Do 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