1414import java .time .temporal .ChronoUnit ;
1515import java .util .*;
1616import java .util .regex .Pattern ;
17+ import java .util .regex .PatternSyntaxException ;
1718import java .util .logging .Logger ;
1819
1920@ BotCommand
2021@ CommandPkg (
2122 label = "Clean Directory" ,
22- node_label = "{{inputFolderPath}} by deleting {{selectMethod}} {{recursive }}" ,
23- description = "Remove files/ folders based on rule set " ,
23+ node_label = "Clean {{inputFolderPath}} by deleting {{selectMethod}} older than {{thresholdNumber}} {{thresholdUnit }}" ,
24+ description = "Delete old files and folders based on age threshold. Useful for cleaning logs, temp files, and backups. " ,
2425 icon = "delete_folders.svg" ,
2526 name = "device_delete_files_folders" ,
2627 group_label = "Device" ,
@@ -42,90 +43,93 @@ public class DeleteFilesFolders {
4243 @ Execute
4344 public void action (
4445 @ Idx (index = "1" , type = AttributeType .FILE )
45- @ Pkg (label = "Enter base folder path " , description = "Files/Folders will be scanned within this folder " +
46- "for deletion " )
46+ @ Pkg (label = "Target folder to clean " , description = "Base folder path where cleanup will be performed. " +
47+ "This folder itself will never be deleted, only its contents. " )
4748 @ NotEmpty
4849 @ FileFolder
4950 String inputFolderPath ,
5051
5152 @ Idx (index = "2" , type = AttributeType .SELECT , options = {
52- @ Idx .Option (index = "2.1" , pkg = @ Pkg (label = "Directories and Files " , value = PROCESS_ALL_TYPES ,
53- node_label = "it's directories and files " )),
54- @ Idx .Option (index = "2.2" , pkg = @ Pkg (label = "Files Only " , value = PROCESS_ONLY_FILE_TYPE ,
55- node_label = "it's files" ))})
56- @ Pkg (label = "Deletion option " , default_value = PROCESS_ALL_TYPES ,
53+ @ Idx .Option (index = "2.1" , pkg = @ Pkg (label = "Both files and folders " , value = PROCESS_ALL_TYPES ,
54+ node_label = "files and folders " )),
55+ @ Idx .Option (index = "2.2" , pkg = @ Pkg (label = "Files only (keep folder structure) " , value = PROCESS_ONLY_FILE_TYPE ,
56+ node_label = "files only " ))})
57+ @ Pkg (label = "What to delete " , default_value = PROCESS_ALL_TYPES ,
5758 default_value_type = DataType .STRING )
5859 @ NotEmpty
5960 @ SelectModes
6061 String selectMethod ,
6162
6263 @ Idx (index = "3" , type = AttributeType .CHECKBOX )
63- @ Pkg (label = "All subdirectories are searched as well" , default_value = "true" ,
64- node_label = "Action all subdirectories" ,
64+ @ Pkg (label = "Include all subfolders" , default_value = "true" ,
65+ node_label = "including subfolders" ,
66+ description = "When checked: processes all nested folders. When unchecked: only processes immediate folder contents." ,
6567 default_value_type = DataType .BOOLEAN )
6668 Boolean recursive ,
6769
6870 @ Idx (index = "4" , type = AttributeType .NUMBER )
69- @ Pkg (label = "Threshold number " , default_value_type = DataType .NUMBER , default_value = "30" ,
70- description = "any file/folder with threshold age value older than this value will be " +
71- "deleted " )
71+ @ Pkg (label = "Delete items older than " , default_value_type = DataType .NUMBER , default_value = "30" ,
72+ description = "Age threshold for deletion. Example: 7 (with Days selected) = delete items 7+ days old. " +
73+ "Use 0 to delete all items regardless of age. " )
7274 @ NotEmpty
7375 @ GreaterThanEqualTo ("0" )
7476 @ NumberInteger
7577 Number thresholdNumber ,
7678
7779 @ Idx (index = "5" , type = AttributeType .SELECT , options = {
78- @ Idx .Option (index = "5.1" , pkg = @ Pkg (label = "DAY " , value = THRESHOLD_UNIT_DAY )),
79- @ Idx .Option (index = "5.2" , pkg = @ Pkg (label = "HOUR " , value = THRESHOLD_UNIT_HOUR )),
80- @ Idx .Option (index = "5.3" , pkg = @ Pkg (label = "MINUTE " , value = THRESHOLD_UNIT_MINUTE )),
81- @ Idx .Option (index = "5.4" , pkg = @ Pkg (label = "SECOND " , value = THRESHOLD_UNIT_SECOND ))})
82- @ Pkg (label = "Threshold Unit " , default_value = THRESHOLD_UNIT_DAY ,
80+ @ Idx .Option (index = "5.1" , pkg = @ Pkg (label = "Days " , value = THRESHOLD_UNIT_DAY )),
81+ @ Idx .Option (index = "5.2" , pkg = @ Pkg (label = "Hours " , value = THRESHOLD_UNIT_HOUR )),
82+ @ Idx .Option (index = "5.3" , pkg = @ Pkg (label = "Minutes " , value = THRESHOLD_UNIT_MINUTE )),
83+ @ Idx .Option (index = "5.4" , pkg = @ Pkg (label = "Seconds " , value = THRESHOLD_UNIT_SECOND ))})
84+ @ Pkg (label = "Time unit " , default_value = THRESHOLD_UNIT_DAY ,
8385 default_value_type = DataType .STRING )
8486 @ NotEmpty
8587 @ SelectModes
8688 String thresholdUnit ,
8789
8890 @ Idx (index = "6" , type = AttributeType .SELECT , options = {
89- @ Idx .Option (index = "6.1" , pkg = @ Pkg (label = "CREATION " , value = THRESHOLD_CRITERIA_CREATION )),
90- @ Idx .Option (index = "6.2" , pkg = @ Pkg (label = "LAST MODIFICATION " , value =
91+ @ Idx .Option (index = "6.1" , pkg = @ Pkg (label = "Creation date (when file was created) " , value = THRESHOLD_CRITERIA_CREATION )),
92+ @ Idx .Option (index = "6.2" , pkg = @ Pkg (label = "Last modified date (when file was last changed) " , value =
9193 THRESHOLD_CRITERIA_MODIFICATION ))})
92- @ Pkg (label = "Threshold age type" , default_value = THRESHOLD_CRITERIA_CREATION ,
93- default_value_type = DataType .STRING )
94+ @ Pkg (label = "Age based on" , default_value = THRESHOLD_CRITERIA_MODIFICATION ,
95+ default_value_type = DataType .STRING ,
96+ description = "For logs, use 'Last modified' as they are continuously updated." )
9497 @ NotEmpty
9598 @ SelectModes
9699 String thresholdCriteria ,
97100
98101 @ Idx (index = "7" , type = AttributeType .CHECKBOX )
99- @ Pkg (label = "Ignore specific folder paths " , default_value = "false" , default_value_type =
102+ @ Pkg (label = "Skip specific folders (preserve them) " , default_value = "false" , default_value_type =
100103 DataType .BOOLEAN )
101104 Boolean skipFolders ,
102105
103106 @ Idx (index = "7.1" , type = AttributeType .TEXT )
104- @ Pkg (label = "Regex pattern to match folder paths to ignore " ,
105- description = ".*\\ \\ subDirectory" + " to skip folder called subDirectory on windows platform " +
106- "Matching will be done on absolute path in OS file separator format ." )
107+ @ Pkg (label = "Folder pattern to skip (regex) " ,
108+ description = "Examples: ' .*\\ \\ backup$' skips folders named 'backup', '.* \\ \\ (archive|important).*' skips folders containing 'archive' or 'important'. " +
109+ "Pattern matches against full absolute path." )
107110 @ NotEmpty
108111 String skipFolderPathPattern ,
109112
110113 @ Idx (index = "8" , type = AttributeType .CHECKBOX )
111- @ Pkg (label = "Ignore specific file paths " , default_value = "false" , default_value_type =
114+ @ Pkg (label = "Skip specific files (preserve them) " , default_value = "false" , default_value_type =
112115 DataType .BOOLEAN )
113116 Boolean skipFiles ,
114117
115118 @ Idx (index = "8.1" , type = AttributeType .TEXT )
116- @ Pkg (label = "Regex pattern to match file paths to ignore " , description =
117- ".*\\ .txt$" + " to skip all text files on windows platform. Matching will be done on absolute " +
118- "path in OS file separator format ." )
119+ @ Pkg (label = "File pattern to skip (regex) " , description =
120+ "Examples: ' .*\\ .log$' skips .log files, '.* \\ .(txt|csv)$' skips .txt and .csv files, '.*important.*' skips files with 'important' in name. " +
121+ "Pattern matches against full absolute path ." )
119122 @ NotEmpty
120123 String skipFilePathPattern ,
121124
122125 @ Idx (index = "9" , type = AttributeType .RADIO , options = {
123- @ Idx .Option (index = "9.1" , pkg = @ Pkg (label = "Throw error" , value = ERROR_THROW )),
124- @ Idx .Option (index = "9.2" , pkg = @ Pkg (label = "Ignore " , value = ERROR_IGNORE ))
126+ @ Idx .Option (index = "9.1" , pkg = @ Pkg (label = "Stop and throw error (fail the bot) " , value = ERROR_THROW )),
127+ @ Idx .Option (index = "9.2" , pkg = @ Pkg (label = "Continue silently (skip locked files) " , value = ERROR_IGNORE ))
125128 })
126- @ Pkg (label = "If certain files/folders cannot be deleted" , default_value_type = DataType .STRING ,
129+ @ Pkg (label = "When files cannot be deleted (locked/in-use) " , default_value_type = DataType .STRING ,
127130 description =
128- "Behavior in case a file is locked/missing permission" , default_value = ERROR_IGNORE )
131+ "Choose 'Continue silently' for log cleanup where some files may be actively written. Choose 'Stop and throw error' when all files must be deleted." ,
132+ default_value = ERROR_IGNORE )
129133 @ NotEmpty
130134 String unableToDeleteBehavior
131135 ) {
@@ -152,7 +156,8 @@ public void action(
152156 collector .getFilesToDelete (),
153157 collector .getDirectoriesToDelete (),
154158 collector .getFilesToSkip (),
155- collector .getDirectoriesToSkip ()
159+ collector .getDirectoriesToSkip (),
160+ collector .getYoungFiles ()
156161 );
157162
158163 // Phase 3: Execute deletions
@@ -225,6 +230,7 @@ private static class FileCollector extends SimpleFileVisitor<Path> {
225230 private final Set <Path > directoriesToDelete = new HashSet <>();
226231 private final Set <Path > filesToSkip = new HashSet <>();
227232 private final Set <Path > directoriesToSkip = new HashSet <>();
233+ private final Set <Path > youngFiles = new HashSet <>(); // Track files too young to delete
228234
229235 public FileCollector (
230236 Path basePath ,
@@ -287,6 +293,9 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
287293 if (meetsDeletionCriteria (attrs )) {
288294 LOGGER .info ("Marking file for deletion: " + file );
289295 filesToDelete .add (file );
296+ } else {
297+ // Track files that are too young to delete
298+ youngFiles .add (file );
290299 }
291300
292301 return FileVisitResult .CONTINUE ;
@@ -311,11 +320,17 @@ private boolean meetsDeletionCriteria(BasicFileAttributes attrs) {
311320 throw new IllegalArgumentException ("Unsupported threshold criteria: " + thresholdCriteria );
312321 }
313322
314- return fileTime .isBefore (deletionThresholdInstant ); // older than threshold
323+ // Files at or before the threshold instant should be deleted
324+ // Using !isAfter instead of isBefore to include files exactly at the threshold
325+ return !fileTime .isAfter (deletionThresholdInstant );
315326 }
316327
317328 private boolean matchesPattern (String pathString , String pattern ) {
318- return Pattern .matches (pattern , pathString );
329+ try {
330+ return Pattern .matches (pattern , pathString );
331+ } catch (PatternSyntaxException e ) {
332+ throw new BotCommandException ("Invalid regex pattern: '" + pattern + "'. " + e .getMessage ());
333+ }
319334 }
320335
321336 public Set <Path > getFilesToDelete () {
@@ -333,6 +348,10 @@ public Set<Path> getFilesToSkip() {
333348 public Set <Path > getDirectoriesToSkip () {
334349 return directoriesToSkip ;
335350 }
351+
352+ public Set <Path > getYoungFiles () {
353+ return youngFiles ;
354+ }
336355 }
337356
338357 /**
@@ -350,7 +369,8 @@ public DeletionProcessor(
350369 Set <Path > filesToDelete ,
351370 Set <Path > directoriesToDelete ,
352371 Set <Path > filesToSkip ,
353- Set <Path > directoriesToSkip ) {
372+ Set <Path > directoriesToSkip ,
373+ Set <Path > youngFiles ) {
354374 this .basePath = basePath ;
355375 this .filesToDelete = new HashSet <>(filesToDelete );
356376 this .directoriesToDelete = new HashSet <>(directoriesToDelete );
@@ -369,6 +389,11 @@ public DeletionProcessor(
369389 addParentsToPreserveList (skippedDir );
370390 }
371391
392+ // Process young files - preserve their parent directories
393+ for (Path youngFile : youngFiles ) {
394+ addParentsToPreserveList (youngFile );
395+ }
396+
372397 // Remove preserved directories from deletion list
373398 this .directoriesToDelete .removeAll (directoriesToPreserve );
374399
0 commit comments