1212import java .nio .file .attribute .BasicFileAttributes ;
1313import java .time .Instant ;
1414import java .time .temporal .ChronoUnit ;
15- import java .util .HashSet ;
16- import java .util .Set ;
15+ import java .util .*;
1716import java .util .regex .Pattern ;
17+ import java .util .logging .Logger ;
1818
1919@ BotCommand
2020@ CommandPkg (
@@ -37,6 +37,7 @@ public class DeleteFilesFolders {
3737 private static final String PROCESS_ALL_TYPES = "ALL" ;
3838 private static final String ERROR_THROW = "THROW" ;
3939 private static final String ERROR_IGNORE = "IGNORE" ;
40+ private static final Logger LOGGER = Logger .getLogger (DeleteFilesFolders .class .getName ());
4041
4142 @ Execute
4243 public void action (
@@ -130,67 +131,51 @@ public void action(
130131 ) {
131132 try {
132133 Path basePath = Paths .get (inputFolderPath );
133- Instant deletionThresholdInstant = calculateAgeThreshold (thresholdNumber .longValue (), thresholdUnit );
134- Set <Path > directoriesToSkipDeletion = new HashSet <>();
135- Set <Path > filesToSkipDeletion = new HashSet <>();
136- Set <Path > directoriesToDelete = new HashSet <>();
137- Set <Path > filesToDelete = new HashSet <>();
138-
139- directoriesToSkipDeletion .add (basePath );
140- Files .walkFileTree (basePath , new SimpleFileVisitor <>() {
141- @ Override
142- public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) {
143- if (!recursive ) {
144- return FileVisitResult .SKIP_SUBTREE ;
145- }
146-
147- if (skipFolders && shouldSkipDir (dir , skipFolderPathPattern )) {
148- directoriesToSkipDeletion .add (basePath );
149- return FileVisitResult .SKIP_SUBTREE ;
150- }
151- if (meetsDeletionCriteria (attrs , thresholdCriteria , deletionThresholdInstant )) {
152- directoriesToDelete .add (dir );
153- }
154- return FileVisitResult .CONTINUE ;
155- }
134+ if (!Files .exists (basePath )) {
135+ throw new BotCommandException ("Base folder path does not exist: " + inputFolderPath );
136+ }
156137
157- @ Override
158- public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) {
159- if (skipFiles && shouldSkipFile (file , skipFilePathPattern )) {
160- filesToSkipDeletion .add (file );
161- return FileVisitResult .CONTINUE ;
162- }
163- if (meetsDeletionCriteria (attrs , thresholdCriteria , deletionThresholdInstant )) {
164- filesToDelete .add (file );
165- }
166- return FileVisitResult .CONTINUE ;
167- }
138+ LOGGER .info ("Starting deletion process for base path: " + basePath );
168139
169- });
170- //add all ancestors to skip to ensure they are not deleted
171- for (Path fileToSkip : filesToSkipDeletion ) {
172- Path parent = fileToSkip .getParent ();
173- while (parent != null && !parent .equals (basePath )) {
174- directoriesToSkipDeletion .add (parent );
175- parent = parent .getParent ();
176- }
140+ // Phase 1: Collect data - files/directories to delete and skip
141+ FileCollector collector = new FileCollector (
142+ basePath , recursive , thresholdCriteria ,
143+ calculateAgeThreshold (thresholdNumber .longValue (), thresholdUnit ),
144+ skipFiles , skipFilePathPattern ,
145+ skipFolders , skipFolderPathPattern
146+ );
147+ Files .walkFileTree (basePath , collector );
148+
149+ // Phase 2: Process the data - resolve conflicts between delete and skip lists
150+ DeletionProcessor processor = new DeletionProcessor (
151+ basePath ,
152+ collector .getFilesToDelete (),
153+ collector .getDirectoriesToDelete (),
154+ collector .getFilesToSkip (),
155+ collector .getDirectoriesToSkip ()
156+ );
157+
158+ // Phase 3: Execute deletions
159+ if (selectMethod .equalsIgnoreCase (PROCESS_ONLY_FILE_TYPE ) ||
160+ selectMethod .equalsIgnoreCase (PROCESS_ALL_TYPES )) {
161+ delete (processor .getFilesToDelete (), unableToDeleteBehavior );
177162 }
178- directoriesToDelete . removeAll ( directoriesToSkipDeletion );
163+
179164 if (selectMethod .equalsIgnoreCase (PROCESS_ALL_TYPES )) {
180- delete (filesToDelete , unableToDeleteBehavior );
181- delete (directoriesToDelete , unableToDeleteBehavior );
182- } else if (selectMethod .equalsIgnoreCase (PROCESS_ONLY_FILE_TYPE )) {
183- delete (filesToDelete , unableToDeleteBehavior );
165+ delete (processor .getSortedDirectoriesToDelete (), unableToDeleteBehavior );
184166 }
185167
168+ LOGGER .info ("Deletion process completed successfully" );
169+
186170 } catch (IOException e ) {
171+ LOGGER .severe ("IO error occurred: " + e .getMessage ());
187172 if (unableToDeleteBehavior .equalsIgnoreCase (ERROR_THROW )) {
188- throw new BotCommandException (e .getMessage ());
173+ throw new BotCommandException ("IO error: " + e .getMessage (), e );
189174 }
190175 } catch (Exception e ) {
191- throw new BotCommandException (e .getMessage ());
176+ LOGGER .severe ("Unexpected error: " + e .getMessage ());
177+ throw new BotCommandException ("Error: " + e .getMessage (), e );
192178 }
193-
194179 }
195180
196181 private Instant calculateAgeThreshold (long threshold , String unit ) {
@@ -209,41 +194,206 @@ private Instant calculateAgeThreshold(long threshold, String unit) {
209194 }
210195 }
211196
212- private boolean shouldSkipDir (Path path , String folderPattern ) {
213- return Files .isDirectory (path ) && Pattern .matches (folderPattern , path .toString ());
197+ private void delete (List <Path > pathsToDelete , String unableToDeleteBehavior ) {
198+ for (Path path : pathsToDelete ) {
199+ try {
200+ LOGGER .info ("Deleting: " + path );
201+ FileUtils .forceDelete (path .toFile ());
202+ } catch (IOException e ) {
203+ LOGGER .warning ("Failed to delete " + path + ": " + e .getMessage ());
204+ if (unableToDeleteBehavior .equalsIgnoreCase (ERROR_THROW )) {
205+ throw new BotCommandException ("Failed to delete " + path + ": " + e .getMessage (), e );
206+ }
207+ }
208+ }
214209 }
215210
216- private boolean meetsDeletionCriteria (BasicFileAttributes attrs , String thresholdCriteria , Instant ageThreshold ) {
217- Instant fileTime ;
218- switch (thresholdCriteria ) {
219- case THRESHOLD_CRITERIA_CREATION :
220- fileTime = attrs .creationTime ().toInstant ();
221- break ;
222- case THRESHOLD_CRITERIA_MODIFICATION :
223- fileTime = attrs .lastModifiedTime ().toInstant ();
224- break ;
225- default :
226- throw new IllegalArgumentException ("Unsupported threshold criteria: " + thresholdCriteria );
211+ /**
212+ * Helper class that collects files and directories during file tree traversal.
213+ */
214+ private class FileCollector extends SimpleFileVisitor <Path > {
215+ private final Path basePath ;
216+ private final boolean recursive ;
217+ private final String thresholdCriteria ;
218+ private final Instant deletionThresholdInstant ;
219+ private final boolean skipFiles ;
220+ private final String skipFilePathPattern ;
221+ private final boolean skipFolders ;
222+ private final String skipFolderPathPattern ;
223+
224+ private final Set <Path > filesToDelete = new HashSet <>();
225+ private final Set <Path > directoriesToDelete = new HashSet <>();
226+ private final Set <Path > filesToSkip = new HashSet <>();
227+ private final Set <Path > directoriesToSkip = new HashSet <>();
228+
229+ public FileCollector (
230+ Path basePath ,
231+ boolean recursive ,
232+ String thresholdCriteria ,
233+ Instant deletionThresholdInstant ,
234+ boolean skipFiles ,
235+ String skipFilePathPattern ,
236+ boolean skipFolders ,
237+ String skipFolderPathPattern ) {
238+ this .basePath = basePath ;
239+ this .recursive = recursive ;
240+ this .thresholdCriteria = thresholdCriteria ;
241+ this .deletionThresholdInstant = deletionThresholdInstant ;
242+ this .skipFiles = skipFiles ;
243+ this .skipFilePathPattern = skipFilePathPattern ;
244+ this .skipFolders = skipFolders ;
245+ this .skipFolderPathPattern = skipFolderPathPattern ;
227246 }
228247
229- return fileTime .isBefore (ageThreshold );//older than threshold deletion date
230- }
248+ @ Override
249+ public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) {
250+ // Skip processing of the base path for deletion (never delete the base path)
251+ if (dir .equals (basePath )) {
252+ return FileVisitResult .CONTINUE ;
253+ }
231254
232- private boolean shouldSkipFile (Path path , String filePattern ) {
233- return Files .isRegularFile (path ) && Pattern .matches (filePattern , path .toFile ().getAbsolutePath ());
234- }
255+ // Check if this directory should be skipped based on pattern
256+ if (skipFolders && matchesPattern (dir .toFile ().getAbsolutePath (), skipFolderPathPattern )) {
257+ LOGGER .info ("Skipping directory based on pattern: " + dir );
258+ directoriesToSkip .add (dir );
259+ return FileVisitResult .SKIP_SUBTREE ; // Don't process contents of skipped directories
260+ }
235261
236- private static void delete (Set <Path > filesToDelete ,
237- String unableToDeleteBehavior ) {
238- for (Path filePath : filesToDelete ) {
239- try {
240- FileUtils .forceDelete (filePath .toFile ());
241- } catch (IOException e ) {
242- if (unableToDeleteBehavior .equalsIgnoreCase (ERROR_THROW )) {
243- throw new BotCommandException (e .getMessage ());
244- }
262+ // Check if this directory meets the age criteria for deletion
263+ if (meetsDeletionCriteria (attrs )) {
264+ LOGGER .info ("Marking directory for potential deletion: " + dir );
265+ directoriesToDelete .add (dir );
266+ }
267+
268+ // If not recursive, skip subdirectories (unless it's the base path)
269+ if (!recursive && !dir .equals (basePath )) {
270+ return FileVisitResult .SKIP_SUBTREE ;
271+ }
272+
273+ return FileVisitResult .CONTINUE ;
274+ }
275+
276+ @ Override
277+ public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) {
278+ // Check if this file should be skipped based on pattern
279+ if (skipFiles && matchesPattern (file .toFile ().getAbsolutePath (), skipFilePathPattern )) {
280+ LOGGER .info ("Skipping file based on pattern: " + file );
281+ filesToSkip .add (file );
282+ return FileVisitResult .CONTINUE ;
283+ }
284+
285+ // Check if this file meets the age criteria for deletion
286+ if (meetsDeletionCriteria (attrs )) {
287+ LOGGER .info ("Marking file for deletion: " + file );
288+ filesToDelete .add (file );
245289 }
290+
291+ return FileVisitResult .CONTINUE ;
292+ }
293+
294+ @ Override
295+ public FileVisitResult visitFileFailed (Path file , IOException exc ) {
296+ LOGGER .warning ("Failed to visit file: " + file + " - " + exc .getMessage ());
297+ return FileVisitResult .CONTINUE ;
298+ }
299+
300+ private boolean meetsDeletionCriteria (BasicFileAttributes attrs ) {
301+ Instant fileTime ;
302+ switch (thresholdCriteria ) {
303+ case THRESHOLD_CRITERIA_CREATION :
304+ fileTime = attrs .creationTime ().toInstant ();
305+ break ;
306+ case THRESHOLD_CRITERIA_MODIFICATION :
307+ fileTime = attrs .lastModifiedTime ().toInstant ();
308+ break ;
309+ default :
310+ throw new IllegalArgumentException ("Unsupported threshold criteria: " + thresholdCriteria );
311+ }
312+
313+ return fileTime .isBefore (deletionThresholdInstant ); // older than threshold
314+ }
315+
316+ private boolean matchesPattern (String pathString , String pattern ) {
317+ return Pattern .matches (pattern , pathString );
318+ }
319+
320+ public Set <Path > getFilesToDelete () {
321+ return filesToDelete ;
322+ }
323+
324+ public Set <Path > getDirectoriesToDelete () {
325+ return directoriesToDelete ;
326+ }
327+
328+ public Set <Path > getFilesToSkip () {
329+ return filesToSkip ;
330+ }
331+
332+ public Set <Path > getDirectoriesToSkip () {
333+ return directoriesToSkip ;
246334 }
247335 }
248336
337+ /**
338+ * Helper class that processes file and directory lists to resolve conflicts
339+ * and prepare for deletion.
340+ */
341+ private class DeletionProcessor {
342+ private final Path basePath ;
343+ private final Set <Path > filesToDelete ;
344+ private final Set <Path > directoriesToDelete ;
345+ private final Set <Path > directoriesToPreserve = new HashSet <>();
346+
347+ public DeletionProcessor (
348+ Path basePath ,
349+ Set <Path > filesToDelete ,
350+ Set <Path > directoriesToDelete ,
351+ Set <Path > filesToSkip ,
352+ Set <Path > directoriesToSkip ) {
353+ this .basePath = basePath ;
354+ this .filesToDelete = new HashSet <>(filesToDelete );
355+ this .directoriesToDelete = new HashSet <>(directoriesToDelete );
356+
357+ // Always preserve the base path
358+ directoriesToPreserve .add (basePath );
359+
360+ // Process skipped files - preserve their parent directories
361+ for (Path skippedFile : filesToSkip ) {
362+ addParentsToPreserveList (skippedFile );
363+ }
364+
365+ // Process skipped directories - preserve them and their parent directories
366+ for (Path skippedDir : directoriesToSkip ) {
367+ directoriesToPreserve .add (skippedDir );
368+ addParentsToPreserveList (skippedDir );
369+ }
370+
371+ // Remove preserved directories from deletion list
372+ this .directoriesToDelete .removeAll (directoriesToPreserve );
373+
374+ LOGGER .info ("Files to delete after processing: " + this .filesToDelete .size ());
375+ LOGGER .info ("Directories to delete after processing: " + this .directoriesToDelete .size ());
376+ LOGGER .info ("Directories preserved for containing skipped items: " +
377+ (directoriesToPreserve .size () - 1 )); // -1 for basePath
378+ }
379+
380+ private void addParentsToPreserveList (Path path ) {
381+ Path parent = path .getParent ();
382+ while (parent != null && !parent .equals (basePath )) {
383+ directoriesToPreserve .add (parent );
384+ parent = parent .getParent ();
385+ }
386+ }
387+
388+ public List <Path > getFilesToDelete () {
389+ return new ArrayList <>(filesToDelete );
390+ }
391+
392+ public List <Path > getSortedDirectoriesToDelete () {
393+ List <Path > sortedDirectories = new ArrayList <>(directoriesToDelete );
394+ // Sort by depth (descending) - delete deepest directories first
395+ sortedDirectories .sort ((p1 , p2 ) -> Integer .compare (p2 .getNameCount (), p1 .getNameCount ()));
396+ return sortedDirectories ;
397+ }
398+ }
249399}
0 commit comments