File Coverage

blib/lib/File/CleanupTask.pm
Criterion Covered Total %
statement 455 524 86.8
branch 147 206 71.3
condition 54 75 72.0
subroutine 44 47 93.6
pod 6 6 100.0
total 706 858 82.2


line stmt bran cond sub pod time code
1             package File::CleanupTask;
2              
3 4     4   67293 use strict;
  4         12  
  4         199  
4 4     4   24 use warnings;
  4         6  
  4         178  
5              
6 4     4   22 use Cwd qw/realpath getcwd chdir/;
  4         12  
  4         478  
7 4     4   27 use File::Path qw/mkpath rmtree/;
  4         8  
  4         283  
8 4     4   28 use File::Basename qw/fileparse/;
  4         8  
  4         343  
9 4     4   26 use File::Spec qw/catpath splitpath/;
  4         7  
  4         123  
10 4     4   2387 use Config::Simple;
  4         84741  
  4         43  
11 4     4   3192 use File::Which qw/which/;
  4         4013  
  4         375  
12 4     4   3372 use Getopt::Long;
  4         47395  
  4         28  
13 4     4   763 use File::Find;
  4         5  
  4         220  
14 4     4   2612 use File::Copy;
  4         9430  
  4         334  
15 4     4   2681 use IPC::Run3 qw/run3/;
  4         85321  
  4         380  
16 4     4   2623 use Sort::Key qw/nkeysort/;
  4         12587  
  4         346  
17 4     4   53 use Config;
  4         6  
  4         22017  
18              
19             =head1 NAME
20              
21             File::CleanupTask - Delete or back up files using a task-based configuration
22              
23             =head1 VERSION
24              
25             Version 0.11
26              
27             =cut
28              
29             our $VERSION = '0.11';
30              
31              
32             =head1 SYNOPSIS
33              
34             use File::CleanupTask;
35              
36             my $cleanup = File::Cleanup->new({
37             conf => "/path/to/tasks_file.tasks",
38             taskname => "TASK_LABEL_IN_TASKFILE",
39             });
40              
41             $cleanup->run();
42              
43             Once run() is called, the cleanup operation 'TASK_LABEL_IN_TASKFILE' specified
44             in tasks_file.tasks is performed.
45              
46              
47             =head2 CONFIGURATION FORMAT
48              
49             A .tasks file is a text file in which one or more cleanup tasks are specified.
50             Each task has a label and a list of options specified as shown in the following
51             example:
52              
53             [TASK_LABEL_IN_TASKFILE]
54             path = '/home/savio/results/'
55             backup_path = '/home/savio/old_results/'
56             backup_gzip = 1
57             max_days = 3
58             recursive = 1
59             prune_empty_directories = 1
60             keep_if_linked_in = '/home/savio/results/'
61              
62             [ANOTHER_LABEL]
63             path = 'C:\\this\\is\\a\\windows\\path'
64             ...
65              
66              
67             In this case, [TASK_LABEL_IN_TASKFILE] is the name of the cleanup task to be
68             executed.
69              
70             The following options can be specified under a task label:
71              
72             =head3 path
73              
74             The path to the directory containing the files to be deleted or removed.
75              
76             Note that for MS Windows the backslashes in a path should be escaped and single
77             quotes are strictly needed when specifying a path name (see the example above).
78              
79             =head3 backup_path
80              
81             If specified, will cause files to be moved in the specified directory instead
82             of being deleted. If backup_path doesn't exist, it will be created. Symlinks
83             are not backed up. The files are backed up at the toplevel of backup_path in a
84             .gz (or .tgz, depending on backup_gzip) archive, which preserves pathnames of
85             the archived files.
86              
87             =head3 backup_gzip
88              
89             If set to "1", will gzip the files saved in backup_path. The resulting archive
90             will preserve the pathname of the original file, and will be relative to
91             'path'.
92              
93             For example, given the following configuration:
94              
95             [LABEL]
96             path = /path/to/cleanup/
97             backup_path = /path/to/backup/
98             backup_gzip = 1
99              
100             If /path/to/cleanup/my/target/file.txt is encountered, and it's old, it will be
101             backed up in /path/to/backup/file.txt.gz. Uncompressing file.txt.gz using
102             /path/to/backup as current working directory will result in:
103              
104             /path/to/backup/path/to/cleanup/my/target/file.txt
105              
106              
107             =head3 max_days
108              
109             The number of maximum days within which the files in the cleanup directories
110             are kept. If a file is older than the specified number of days, it is queued
111             for deletion.
112              
113             For example, max_days = 3 will delete files older than 3 days from the cleanup
114             directory.
115              
116             max_days defaults to 0 if it isn't specified, meaning that all the files are to
117             be deleted.
118              
119             =head3 recursive
120              
121             If set to 0, only files within "path" can be deleted/backed up.
122             If set to 1, files located at any level within "path" can be deleted.
123              
124             If C is enabled and C is disabled, then
125             only empty directories that are direct children of "path" will be cleaned up.
126              
127             By default, this takes the 0 value.
128              
129             =head3 prune_empty_directories
130              
131             If set to 1, empty directories will be deleted, respecting the C
132             option. (In versions 0.09 and older, this would not respect the max_days option.)
133              
134             By default, this takes the 0 value.
135              
136             =head3 keep_if_linked_in
137              
138             A pathname to a directory that may contain symlinks. If specified, it will
139             prevent deletion of files and directories within path that are symlinked in
140             this directory, regardless of their age.
141              
142             This option will be ignored in MS Windows or in other operating systems that
143             don't support symlinks.
144              
145             =head3 do_not_delete
146              
147             A regular expression that defines a pattern to look for. Any pathnames matching
148             this pattern will not be erased, regardless of their age. The regular
149             expression applies to the full pathname of the file or directory.
150              
151             In the configuration file, it should be surrounded by forward slashes. Because
152             the configuration file is parsed by L, you will need to escape
153             any backslashes in the regex with a backslash.
154              
155             =cut
156              
157             =head3 delete_all_or_nothing_in
158              
159             If set to 1, immediate subfolders in path will be deleted only if all the files
160             in it are deleted.
161              
162             =head3 pattern
163              
164             If specified, will apply any potential delete or backup action to the files
165             that match the pattern. Any other file will be left untouched.
166              
167             =cut
168              
169             =head3 enable_symlinks_integrity_in_path
170              
171             If set to 1, the symlinks inside 'path' will be deleted only if their target
172             will be deleted. This option is disabled by default, which means that the
173             target of symlinks within the path will not be questioned during
174             deletion/backup, they will be just treated as regular files.
175              
176             This option will be ignored in MS Windows or in other operating systems that
177             don't support symlinks.
178              
179             =cut
180              
181              
182             =head1 METHODS
183              
184              
185              
186             =head2 new
187              
188             Create and configure a new File::CleanupTask object.
189              
190             The object must be initialised as follows:
191              
192             my $cleanup = File::Cleanup->new({
193             conf => "/path/to/tasks_file.tasks",
194             taskname => 'TASK_LABEL_IN_TASKFILE',
195             });
196              
197             =cut
198              
199             sub new {
200 21     21 1 178348 my $class = shift;
201 21         58 my $params = shift;
202 21         67 my $self = { params => $params };
203 21         203 $self->{config_simple} = new Config::Simple;
204              
205 21         889 $self->{cmd_gzip} = File::Which::which('gzip');
206 21 50       3768 if (!$self->{cmd_gzip}) {
207 0         0 $self->_warn(
208             "No gzip executable found in your path."
209             . " Option backup_gzip will be disabled!"
210             );
211             }
212 21         117 return bless $self, $class;
213             }
214              
215             =head2 command_line_run
216              
217             Given the arguments specified in the command line, processes them,
218             creates a new File::CleanupTask object, and then calls C.
219              
220             Options include I, I, I and I.
221              
222              
223             =over
224              
225             =item I: just build and show the plan, nothing will be executed or deleted.
226              
227             =item I: produce more verbose output.
228              
229             =item I: optional, will result in the execution of the specified task.
230              
231             =item I: the path to the .tasks configuration file.
232              
233             =back
234              
235             =cut
236              
237             sub command_line_run {
238 0     0 1 0 my $class = shift;
239 0         0 my $rh_params = {};
240              
241 0 0       0 GetOptions(
242             $rh_params,
243             'conf=s', # The path to the task configuration file
244             'taskname|task=s', # The name of the task to be executed (must be
245             # included in the configuration)
246              
247             'dryrun',
248             'verbose',
249             'help',
250             )
251             || $class->_usage_and_exit();
252              
253 0 0       0 if ( $rh_params->{help} ) {
254 0         0 $class->_usage_and_exit();
255             }
256              
257 0 0       0 if ( !$rh_params->{conf} ) {
258 0         0 $class->_usage_and_exit('Parameter --conf required');
259             }
260              
261 0 0       0 if ( $rh_params->{dryrun} ) {
262 0         0 $rh_params->{verbose} = 1; # Implicitly turn on verbose
263             }
264              
265 0         0 $class->new($rh_params)->run();
266              
267             }
268              
269              
270             =head2 run
271              
272             Perform the cleanup
273              
274             =cut
275              
276             sub run {
277              
278 20     20 1 1948953 my $can_symlink = eval { symlink("",""); 1 };
  20         74  
  20         45  
279              
280 20         46 my $self = shift;
281 20         75 my @compulsory_values = (qw/path max_days/);
282 20         287 my %allowed_values = (
283             'max_days' => '',
284             'recursive' => '',
285             'prune_empty_directories' => '',
286             'path' => '',
287             'keep_if_linked_in' => '',
288             'backup_gzip' => '',
289             'backup_path' => '',
290             'do_not_delete' => '',
291             'delete_all_or_nothing_in' => '',
292             'pattern' => '',
293             'enable_symlinks_integrity_in_path' => '',
294             );
295              
296             ##
297             ## Read tasks file
298             ##
299 20         88 my $config_file = $self->{params}{conf};
300 20 50       444 if ( !-e $config_file ) {
301 0         0 $self->_usage_and_exit("Config file $config_file does not exist");
302             }
303              
304 20         162 $self->{config_simple}->read($config_file);
305            
306 20         110867 my %taskfile = $self->{config_simple}->vars();
307 20         13319 foreach my $line ( keys %taskfile ) {
308 2120         3712 my ($taskname, $key) = split( /[.]/, $line );
309 2120         2578 my $value = $taskfile{$line};
310              
311 2120 50       3571 if (!exists($allowed_values{$key})) {
312 0         0 $self->_usage_and_exit(
313             "Unrecognised configuration option! '$key' was not recognised!"
314             . " Check $self->{params}{conf} and try again.\n"
315             );
316             }
317              
318 2120 50 0     3967 if (!$can_symlink
      33        
319             && ($key eq 'enable_symlinks_integrity_in_path'
320             || $key eq 'keep_if_linked_in') ) {
321              
322 0         0 $self->_warn(
323             "The option $key specified for task $taskname will be"
324             . " ignored, as your operating system doesn't support"
325             . " symlinks"
326             );
327              
328             } else {
329 2120         4664 $self->{_rhh_task_configs}{$taskname}{$key} = $value;
330             }
331              
332             }
333              
334             ##
335             ## Check compulsory values are specified
336             ##
337 20         196 foreach my $ckey (@compulsory_values) {
338 40         59 foreach my $taskname (keys %{$self->{_rhh_task_configs}}) {
  40         203  
339 800 50       1603 if (!exists $self->{_rhh_task_configs}{$taskname}{$ckey}) {
340 0         0 $self->_usage_and_exit(
341             "Compulsory $ckey value hasn't been specified in"
342             . " [$taskname] task in $config_file"
343             );
344             }
345             }
346             }
347            
348             ##
349             ## Decide which tasks to perform - run all the tasks specified
350             ## in the configuration by default. Run a single task if it is specified in
351             ## the --task option.
352             ##
353 20         53 my @a_all_tasknames = sort keys %{ $self->{_rhh_task_configs} };
  20         274  
354 20 50       100 if ( $self->{params}{taskname} ) {
355 20 50       60 if ( grep { $_ eq $self->{params}{taskname} } @a_all_tasknames ) {
  400         685  
356 20         99 @a_all_tasknames = ( $self->{params}{taskname} );
357             }
358             else {
359 0         0 $self->_usage_and_exit("No such task: $self->{params}{taskname}"
360             . " in $self->{params}{conf}"
361             );
362             }
363             }
364            
365             ##
366             ## This is set once as soonish as the cleanup starts. We want to keep files
367             ## that are newer than max_days at script run time. If a file is deleted in
368             ## one day, we will keep files newer than 8 days. We expect a cleanup to be
369             ## rescheduled in case more recent files need to be deleted.
370             ##
371 20         91 $self->{time} = time;
372              
373             ##
374             ## Execute each task
375             ##
376 20         47 foreach my $taskname (@a_all_tasknames) {
377 20         138 $self->run_one_task($self->{_rhh_task_configs}{$taskname}, $taskname);
378             }
379 20         99 $self->_info("-++ Cleanup completed ++-");
380             }
381              
382             =head2 run_one_task
383              
384             Run a single cleanup task given its configuration and name. The name is used as
385             a label for possible output and is an optional parameter of this method.
386              
387             This will scan all files and directories in path in a depth first fashion. If a
388             file is encountered a target action is performed based on the state of that file
389             (file or directory, symlinked, old, empty directory...).
390              
391             =cut
392              
393             sub run_one_task {
394 24     24 1 17293 my $self = shift;
395 24         49 my $rh_task_config = shift;
396 24         49 my $taskname = shift;
397            
398 24 50       88 if ($taskname) {
399 24         172 $self->_info(
400             "\n"
401             . "\n"
402             . " ----------------------------------------------\n"
403             . " Task -> [ $taskname ]\n"
404             . " ----------------------------------------------\n"
405             );
406             }
407              
408 24         53 my $all_or_nothing_path = $rh_task_config->{delete_all_or_nothing_in};
409 24         65 my $path = $rh_task_config->{path};
410              
411             ##
412             ## Check that path exists
413             ##
414 24 50       576 if (!-d $path) {
415 0         0 $self->_info("Cannot run this task because the path '$path' doesn't");
416 0         0 $self->_info("exist or is not a directory. Please ignore or provide");
417 0         0 $self->_info("a valid 'path' in your configuration file" );
418 0         0 return;
419             }
420            
421             ##
422             ## Check that delete_all_or_nothing_in path exists
423             ##
424 24 50 66     100 if ($all_or_nothing_path && !-d $all_or_nothing_path) {
425 0         0 $self->_info("Cannot run this task because the path ");
426 0         0 $self->_info("'$all_or_nothing_path' doesn't exist or is not a ");
427 0         0 $self->_info("directory. Please ignore or provide a valid ");
428 0         0 $self->_info("'delete_all_or_nothing_in' in your configuration file");
429 0         0 return;
430             }
431            
432             ##
433             ## Check that delete_all_or_nothing is within the cleanup path
434             ##
435 24 50 66     98 if ($all_or_nothing_path
436             && (index($all_or_nothing_path, $path) < 0)) {
437              
438 0         0 $self->_info("Cannot run this task because the specified");
439 0         0 $self->_info("delete_all_or_nothing path is not a");
440 0         0 $self->_info("subdirectory of 'path'");
441 0         0 return;
442             }
443              
444             ##
445             ## Set the minimum time for deleting files
446             ##
447 24         108 my $max_days = $rh_task_config->{max_days};
448 24 100       116 $self->{keep_above_epoch} = $max_days
449             ? $self->{time} - ( $max_days * 60 * 60 * 24 )
450             : undef;
451              
452             ##
453             ## Build never_delete, a list of vital files/dirs that we really don't want
454             ## to delete.
455             ##
456 24         46 my $path_symlink = $rh_task_config->{keep_if_linked_in};
457 24         73 my $path_backup = $rh_task_config->{backup_path};
458              
459 24         46 my @paths = ();
460 24 100       86 push (@paths, $path_symlink) if ($path_symlink);
461              
462 24         130 my $rh_never_delete = $self->_build_never_delete(\@paths);
463              
464             ##
465             ## Build delete_once_empty, a list of directories that should be deleted
466             ## only if all their content is deleted
467             ##
468 24         45 my $rh_delete_once_empty;
469 24 100       85 if ($all_or_nothing_path) {
470              
471 1         9 $rh_delete_once_empty =
472             $self->_build_delete_once_empty([$all_or_nothing_path]);
473              
474 1         8 $self->_print_delete_once_empty($rh_delete_once_empty);
475             }
476              
477 24 100       70 if ($path_backup) {
478 4 50       11 if (!$self->_ensure_path($path_backup)) {
479 0         0 $self->_info("Cannot create the backup directory!. Terminating.");
480 0         0 return;
481             }
482 4         22 my $cpath_backup = $self->_path_check($path_backup);
483 4         10 $rh_task_config->{backup_path} = $cpath_backup;
484              
485 4         8 $self->_never_delete_add_path(
486             $rh_never_delete,
487             $self->_path_check($cpath_backup)
488             );
489              
490             }
491 24 50       78 if ($path) {
492 24         84 my $cpath = $self->_path_check($path);
493 24         70 $rh_task_config->{path} = $cpath;
494 24         88 $self->_never_delete_add_path($rh_never_delete, $cpath);
495             }
496            
497 24         104 $self->_print_never_delete($rh_never_delete);
498              
499 24         240 my $ra_plan = $self->_build_plan({
500             never_delete => $rh_never_delete,
501             delete_once_empty => $rh_delete_once_empty,
502             config => $rh_task_config,
503             path => $path,
504             });
505            
506 24         172 $self->_print_plan($ra_plan);
507              
508 24         175 $self->_execute_plan({
509             plan => $ra_plan,
510             never_delete => $rh_never_delete,
511             config => $rh_task_config,
512             });
513              
514             }
515              
516             =head2 verbose, dryrun
517              
518             Accessors that will tell you if running in dryrun or verbose mode.
519              
520             =cut
521              
522 2644     2644 1 8401 sub verbose { return $_[0]->{params}{verbose}; }
523 1026     1026 1 2594 sub dryrun { return $_[0]->{params}{dryrun}; }
524              
525             =for _build_delete_once_empty
526             Builds a delete_once_empty of pathnames, each of which should be deleted only if
527             all its files are also deleted.
528              
529             =cut
530              
531             sub _build_delete_once_empty {
532 1     1   3 my $self = shift;
533 1         2 my $rh_paths = shift;
534              
535 1         3 my $rh_delete_once_empty = {};
536 1         11 my $working_directory = Cwd->getcwd();
537              
538 1         3 foreach my $p (@$rh_paths) {
539 1         5 $p = $self->_path_check($p);
540 1         393 foreach my $f (glob "$p/*") {
541 18 50       379 if ( -d $f ) {
542 18         58 $self->_delete_once_empty_add_path($rh_delete_once_empty, $f)
543             }
544             }
545             }
546              
547              
548 1         4 return $rh_delete_once_empty;
549             }
550              
551             =for _build_never_delete
552             Builds a never_delete list of pathnames that shouldn't be deleted at any
553             condition.
554              
555             =cut
556              
557             sub _build_never_delete {
558 24     24   45 my $self = shift;
559 24         40 my $rh_paths = shift;
560              
561 24         62 my $rh_never_delete = {};
562 24         257 my $working_directory = Cwd->getcwd();
563              
564 24         64 foreach my $p (@$rh_paths) {
565             ##
566             ## add the directory itself
567             ##
568 10         42 $p = $self->_path_check($p);
569 10         46 $self->_never_delete_add_path($rh_never_delete, $p);
570              
571 10         433 Cwd::chdir($p);
572 10         3045 foreach my $f (glob "$p/*") {
573              
574 117 100       1586 if ( my $f_target = readlink($f) ) {
575             ##
576             ## add any symlink within the directory
577             ##
578 28         83 $self->_never_delete_add_path($rh_never_delete, $f);
579              
580             ##
581             ## add any target of the symlink shouldn't be deleted.
582             ##
583 28         60 $self->_never_delete_add_path($rh_never_delete, $f_target);
584              
585             ##
586             ## if the target is a directory, add all its children
587             ##
588 28 100       484 if ( -d $f_target ) {
589 20 50       59 if ( $f_target = $self->_path_check($f_target) ) {
590             # Any children of the target shouldn't be deleted at any
591             # cost.
592             find(
593             sub {
594 108     108   318 $self->_never_delete_add_path(
595             $rh_never_delete,
596             $self->_path_check($File::Find::name)
597             );
598             },
599 20         1765 ($f_target)
600             );
601             }
602             }
603             }
604              
605             }
606 10         318 Cwd::chdir($working_directory);
607             }
608              
609              
610 24         69 return $rh_never_delete;
611             }
612              
613             =for _never_delete_add_path
614             Adds a path to the given never_delete list.
615              
616             =cut
617              
618             sub _never_delete_add_path {
619 202     202   290 my $self = shift;
620 202         230 my $rh_never_delete = shift;
621 202         228 my $path = shift;
622              
623 202         374 $path = $self->_path_check($path);
624              
625 202 50       529 if (!$path) {
626 0         0 $self->_warn(
627             "Attempt to add empty path to the never_delete list. Ignoring it."
628             );
629             }
630             else {
631 202         774 $rh_never_delete->{paths}{$path} = 1;
632             }
633              
634 202         3795 return;
635             }
636              
637             =for _delete_once_empty_contains
638             Checks if the given path is contained in the delete_once_empty
639              
640             =cut
641              
642             sub _delete_once_empty_contains {
643 35     35   37 my $self = shift;
644 35         37 my $rh_delete_once_empty = shift;
645 35         36 my $path = shift;
646              
647 35 100       185 return 1 if (exists $rh_delete_once_empty->{paths}{$path});
648              
649 17         68 return 0;
650             }
651              
652             =for _delete_once_empty_add_path
653             Adds a path to the given delete_once_empty.
654              
655             =cut
656              
657             sub _delete_once_empty_add_path {
658 18     18   28 my $self = shift;
659 18         22 my $rh_delete_once_empty = shift;
660 18         24 my $path = shift;
661              
662 18         41 $path = $self->_path_check($path);
663 18 50       69 if (!$path) {
664 0         0 $self->_warn(
665             "Attempt to add empty path to the delete_once_empty. Ignoring it."
666             );
667             }
668             else {
669             # Add the path
670 18         112 $rh_delete_once_empty->{paths}{$path} = 1;
671             }
672             }
673              
674             =for _never_delete_contains
675             Checks if the given path is contained in the never_delete.
676              
677             =cut
678              
679             sub _never_delete_contains {
680 1255     1255   1549 my $self = shift;
681 1255         1161 my $rh_never_delete = shift;
682 1255         1190 my $path = shift;
683              
684 1255 100       4082 return 1 if (exists $rh_never_delete->{paths}{$path});
685 1111         3128 return 0;
686             }
687              
688             =for _path_check
689             Checks up the given path, and returns its absolute representation.
690              
691             =cut
692              
693             sub _path_check {
694 2473     2473   2726 my $self = shift;
695 2473         2564 my $path = shift;
696              
697 2473 50       4692 if (!$path) { $self->_info("No path given to _path_check()"); return; }
  0         0  
  0         0  
698              
699 2473 100       45166 if (-l $path) {
700             ##
701             ## Get the canonical path of the symlink parent and append the symlink
702             ## filename to it.
703             ##
704 29         646 my ($volume,undef,$file) = File::Spec->splitpath($path);
705 29         147 my $parent = $self->_parent_path($path);
706 29         87 my $cparent = $self->_path_check($parent);
707 29         264 return File::Spec->catpath($volume, $cparent, $file);
708             }
709              
710 2444 100       231357 return (-e $path) ? Cwd::realpath($path)
711             : File::Spec->canonpath($path);
712             }
713              
714             =begin _build_plan
715              
716             Plans the actions to be executed on the files in the target path according to:
717              
718             - options in the configuration
719             - the target files
720             - the never_delete
721              
722             All files in the never_delete list can't be deleted.
723              
724             =end _build_plan
725              
726             =cut
727              
728             sub _build_plan {
729 24     24   50 my $self = shift;
730 24         32 my $rh_params = shift;
731              
732 24         47 my $path = $rh_params->{path};
733 24         41 my $rh_never_delete = $rh_params->{never_delete};
734 24         46 my $rh_delete_once_empty = $rh_params->{delete_once_empty};
735 24         56 my $recursive = $rh_params->{config}{recursive};
736 24         50 my $prune_empty = $rh_params->{config}{prune_empty_directories};
737 24         62 my $dont_del_pattern = $rh_params->{config}{do_not_delete};
738              
739 24         66 my $symlinks_integrity =
740             $rh_params->{config}{enable_symlinks_integrity_in_path};
741              
742 24         46 my @plan = (); # holds a list of lists: (['filename','action']). We need a
743             # list as we need to perform these actions in order.
744              
745 24         44 my %summary; # holds the number of files to be deleted vs. the
746             # total number of files for each directory visited.
747              
748             my %empties; # avoid to go into empty dirs again.
749              
750             # If "enable_symlinks_integrity_in_path" is true, any symlink will be
751             # postprocessed, and the plan will be built as symlinks were not existing.
752             #
753             # If this is the case, %sym_integrity will be an hash
754             # key: path to symlink target (canonical)
755             # value: symlink pathname (non canonical)
756 0         0 my %sym_integrity;
757              
758 24 100       60 if ($recursive) {
759             find(
760             { 'bydepth' => 1,
761              
762             'preprocess' => sub {
763 373     373   1046 my @files = @_;
764             ##
765             ## Prepare this directory's summary
766             ##
767 373         926 my $dir = $self->_path_check($File::Find::dir);
768 373 50       1321 if (!exists $summary{$dir}) {
769 373         1631 $summary{$dir}{'nfiles'} = 0;
770 373         842 $summary{$dir}{'ndelete'} = 0;
771             }
772 373         14164 return @files;
773             },
774              
775             'wanted' => sub {
776             ##
777             ## Update actions and collect summary
778             ##
779 1259     1259   1584 my $f = $File::Find::name;
780              
781              
782 1259         1127 my $will_check_integrity;
783 1259 100       2124 if ($symlinks_integrity) {
784              
785 87         218 $will_check_integrity =
786             $self->_postprocess_link(\%sym_integrity, $f);
787             }
788              
789 1259 100       2454 if (!$will_check_integrity) {
790              
791 1254         2643 my $dir = $self->_path_check($File::Find::dir);
792              
793 1254 100       13579 if (!exists $empties{$f}) {
794              
795 971         2637 my @actions =
796 971         1012 @{ $self->_plan_add_actions (
797             \@plan,
798             $f,
799             $rh_params
800             )};
801              
802 971         4807 foreach my $action (@actions) {
803             ##
804             ## count deleted items
805             ##
806 881 50 66     17063 if ($action eq 'delete' && (-f $f || -l $f)) {
      66        
807 746         1989 $summary{$dir}{'ndelete'} += 1;
808             }
809              
810             ## count total items
811 881         20204 $summary{$dir}{'nfiles'}++;
812             }
813             }
814              
815             }
816             },
817              
818             'postprocess' => sub {
819             ##
820             ## Consider deleting a directory given the actions performed on
821             ## the files it contains.
822             ##
823 373     373   859 my $dir = $self->_path_check($File::Find::dir);
824 373         1118 my $nf = $summary{$dir}{'nfiles'};
825 373         722 my $ndel = $summary{$dir}{'ndelete'};
826              
827 373         413 my $action = 'nothing';
828 373         403 my $reason = 'default';
829              
830 373 100       1218 if (!$prune_empty) {
    100          
    100          
831 12         33 ($action, $reason) = ('nothing', 'no prune empty');
832             }
833             elsif ($self->_never_delete_contains($rh_never_delete, $dir)) {
834 43         102 ($action, $reason) = ('nothing', 'never_deleted');
835             }
836             elsif ($ndel < $nf) {
837 24         89 ($action, $reason) = (
838             "nothing",
839             "will contain files ($ndel/$nf deleted)"
840             );
841             }
842             else {
843             ##
844             ## May delete if all these conditions are met:
845             ## - prune_empty is on
846             ## - the directory is or will be empty (all files deleted)
847             ## - the directory is not never_deleted
848             ## - the directory is older than max_days old if specified
849             ##
850              
851              
852             # Delete only if the directory doesn't match the pattern
853 294         292 my $matches;
854 294 100       616 if ($dont_del_pattern) {
855              
856 84         235 $dont_del_pattern =
857             $self->_fix_pattern($dont_del_pattern);
858              
859 84         295 $matches = ($dir =~ m@$dont_del_pattern@gsx)
860             }
861 294 50       412 if ($matches) {
862 0         0 ($action, $reason)
863             = ("nothing", "'do_not_delete' matched");
864             }
865             else {
866 294         5222 my $d_time = (stat($dir))[9]; # mtime
867 294 50 100     1798 if (! defined($d_time)) {
    100          
868 0         0 ($action, $reason) = ('nothing', "unable to stat");
869             }
870             elsif ($self->{keep_above_epoch} &&
871             $d_time >= $self->{keep_above_epoch}) {
872              
873 11         26 ($action, $reason) = ('nothing', "new directory");
874              
875             }
876             else {
877             ##
878             ## Delete the directory
879             ##
880 283 100       750 my $verb = $self->_is_folder_empty($dir) ? 'is'
881             : 'will be';
882              
883 283         1403 ($action, $reason)
884             = ('delete', sprintf('%s empty', $verb));
885              
886 283         982 $empties{$dir} = 1;
887             }
888             }
889             }
890              
891             ##
892             ## Add the action to the plan
893             ##
894 373         1978 $self->_plan_add_action( \@plan,
895             { action => $action,
896             reason => $reason,
897             f_path => $dir,
898             }
899             );
900              
901             ##
902             ## Sum up what we found to the parent directory
903             ##
904 373 50       1271 if ( my $f_parent = $self->_parent_path($dir)) {
905 373         947 $summary{$f_parent}{'nfiles'} += $nf;
906 373         8699 $summary{$f_parent}{'ndelete'} += $ndel;
907             }
908             }
909             },
910              
911 18         438 ($self->_path_check($path)) # The path to visit
912              
913             );
914             }
915             else {
916             ##
917             ## Non recursive
918             ##
919 6         15 my $cpath = $self->_path_check($path);
920 6         573 foreach my $f (glob "$path/*") {
921              
922 21         29 my $will_check_integrity;
923 21 50       41 if ($symlinks_integrity) {
924 0         0 $will_check_integrity =
925             $self->_postprocess_link(\%sym_integrity, $f);
926             }
927              
928 21 50       44 if (!$will_check_integrity) {
929              
930 21         48 $f = $self->_path_check($f);
931              
932             ##
933             ## Update actions
934             ##
935 21         70 $self->_plan_add_actions(\@plan, $f, $rh_params);
936              
937             ##
938             ## Now check if the directory is empty
939             ##
940 21 50 100     336 if ( -d $f &&
      100        
      66        
      33        
      66        
941             $prune_empty &&
942             $self->_is_folder_empty($f) &&
943             (!$self->_never_delete_contains($rh_never_delete, $f)) &&
944             (! $self->{keep_above_epoch} || (stat($f))[9] <= $self->{keep_above_epoch})) {
945              
946              
947 1         8 $self->_plan_add_action( \@plan,
948             { action => 'delete',
949             reason => 'is_empty',
950             f_path => $f,
951             }
952             );
953             }
954             }
955             }
956             }
957              
958             ##
959             ## Now should fix the plan taking internal symlinks into account
960             ##
961 24         674 return $self->_refine_plan(
962             \@plan,
963             { never_delete => $rh_never_delete,
964             delete_once_empty => $rh_delete_once_empty,
965             symlinks => \%sym_integrity
966             }
967             );
968             }
969              
970             =begin _plan_add_actions
971              
972             Given a path to a file and the task configuration options, augment the plan
973             with actions to take on that file.
974              
975             Returns the array containing one or more actions performed.
976              
977             These actions are meant to be performed in reverse sequence on the given file.
978             An empty array_ref is returned if no action is to be performed on the given
979             file.
980              
981             A returned action can be one of: delete, backup.
982              
983             Resulting actions are decided according to one or more of the followings:
984              
985             - options in the configuration
986             - the target files
987             - the never_delete
988              
989             This method works under the assumption that the specified file or directory
990             exists and the user has full permissions on it.
991              
992             =end _plan_add_actions
993              
994             =cut
995              
996             sub _plan_add_actions {
997 992     992   1076 my $self = shift;
998 992         967 my $ra_plan = shift;
999 992         1028 my $f = shift;
1000 992         948 my $rh_params = shift;
1001              
1002 992         1853 my $backup_path = $rh_params->{config}{backup_path};
1003 992         1184 my $dont_del_pattern = $rh_params->{config}{do_not_delete};
1004 992         1245 my $pattern = $rh_params->{config}{pattern};
1005              
1006 992         1499 my @actions = ();
1007              
1008 992         826 my $action; # undef = ignore (note, this is different from "nothing")
1009             my $reason;
1010              
1011              
1012             # deal with directories in the caller
1013 992 100 100     21091 if (-d $f && !-l $f) {
1014             return \@actions
1015 99         407 }
1016              
1017             ## Only deal with files/symlinks from now on
1018             ##
1019              
1020 893 100       2505 if ($self->_never_delete_contains($rh_params->{never_delete}, $f)) {
1021             ##
1022             ## In never_delete
1023             ##
1024 101         181 ($action, $reason) = ('nothing', 'in never_delete');
1025             }
1026             else {
1027             ##
1028             ## Decide if the file must be considered
1029             ##
1030 792         838 my $file_must_be_considered = 1; # default: yes (i.e., may delete it)
1031 792 100       1252 if ($pattern) {
1032 50         121 $pattern = $self->_fix_pattern($pattern);
1033 50         309 $file_must_be_considered = ($f =~ m@$pattern@gsx);
1034             }
1035              
1036             ##
1037             ## Decide if the file must be kept
1038             ##
1039 792         757 my $file_must_be_kept; # default: no (i.e., may delete it)
1040 792 100       1237 if ($dont_del_pattern) {
1041 43         91 $dont_del_pattern = $self->_fix_pattern($dont_del_pattern);
1042 43         324 $file_must_be_kept = ($f =~ m@$dont_del_pattern@gsx);
1043             }
1044              
1045             ##
1046             ## Take decisions
1047             ##
1048 792 100       1228 if (!$file_must_be_considered) {
1049 11         25 ($action, $reason) = ('nothing', "'pattern' did not match");
1050             }
1051             else {
1052 781 100       1023 if ($file_must_be_kept) {
1053 20         56 ($action, $reason) = ('nothing', "'do_not_delete' matched");
1054             }
1055             else {
1056             ##
1057             ## Perform an action on the file (delete/backup) according to
1058             ## the given criteria (max_days for now)
1059             ##
1060              
1061             ## Make sure we get the time from the symlink rather than the
1062             ## linked file (if $f is a symlink)
1063 761         633 my $f_time;
1064 761 100 66     23718 if ($Config{d_lstat} && -l $f) {
1065 4         62 $f_time = (lstat($f))[9];
1066             } else {
1067 757         28156 $f_time = (stat($f))[9];
1068             }
1069 761 50 100     4311 if ( !defined($f_time) ) {
    100          
1070 0         0 ($action, $reason) = ('nothing', "unable to stat");
1071             }
1072             elsif ( $self->{keep_above_epoch}
1073             && $f_time >= $self->{keep_above_epoch} ) {
1074              
1075 4         12 ($action, $reason) = ('nothing', "new file");
1076              
1077             }
1078             else {
1079             ##
1080             ## This is an old file
1081             ##
1082 757 100       1182 if ($backup_path) {
1083 9         16 ($action, $reason) = ('backup', 'old file');
1084             }
1085             else {
1086 748         1589 ($action, $reason) = ('delete', 'old file');
1087             }
1088             }
1089             }
1090             }
1091             }
1092              
1093 893 50       1621 if ($action) {
1094 893         1700 push (@actions, $action);
1095 893         3829 $self->_plan_add_action( $ra_plan ,
1096             { action => $action,
1097             reason => $reason,
1098             f_path => $f
1099             }
1100             );
1101             }
1102              
1103 893         3850 return \@actions;
1104             }
1105              
1106             =for _plan_add_action
1107             Adds the given action to the plan.
1108              
1109             =cut
1110              
1111             sub _plan_add_action {
1112 3847     3847   3924 my $self = shift;
1113 3847         3450 my $ra_plan = shift;
1114 3847         3086 my $rh_action = shift;
1115 3847         3112 my $add_to_top= shift;
1116              
1117             # perl 5.8.9 compatibility
1118 3847 100       5622 $add_to_top = defined $add_to_top ? $add_to_top
1119             : 0;
1120              
1121 3847 100       5147 if ($add_to_top) {
1122 1230         6676 unshift (@$ra_plan,
1123             [ $rh_action->{reason},
1124             $rh_action->{f_path},
1125             $rh_action->{action}
1126             ]
1127             );
1128             }
1129             else {
1130 2617         9534 push (@$ra_plan,
1131             [ $rh_action->{reason},
1132             $rh_action->{f_path},
1133             $rh_action->{action}
1134             ]
1135             );
1136             }
1137             }
1138              
1139             =for _is_folder_empty
1140             Returns 1 if the given folder is empty.
1141              
1142             =cut
1143              
1144             sub _is_folder_empty {
1145 288     288   347 my $self = shift;
1146 288         347 my $dirname = shift;
1147 288 50       18778 opendir(my $dh, $dirname) or die "Not a directory";
1148 288 100       2500 return scalar(grep { $_ ne "." && $_ ne ".." } readdir($dh)) == 0;
  1423         7641  
1149             }
1150              
1151             =for _execute_plan
1152             Execute a plan based on the given task options. Blacklist is passed to make
1153             sure once again that no unwanted files or directories are deleted.
1154              
1155             =cut
1156              
1157             sub _execute_plan {
1158 24     24   41 my $self = shift;
1159 24         32 my $rh_params = shift;
1160              
1161 24         49 my $rh_never_delete = $rh_params->{never_delete};
1162 24         58 my $rh_config = $rh_params->{config};
1163 24         53 my $ra_plan = $rh_params->{plan};
1164              
1165 24         44 my $backup_path = $rh_config->{backup_path};
1166 24         51 my $backup_gzip = $rh_config->{backup_gzip};
1167 24         41 my $path = $rh_config->{path};
1168              
1169 24         234 my $working_directory = Cwd->getcwd();
1170              
1171 24         941 Cwd::chdir($path); # Needed for backup
1172              
1173 24         117 while ( my $ra_plan_item = pop @$ra_plan ) {
1174 1270         3192 my ($desc, $f, $action) = @$ra_plan_item;
1175              
1176 1270 100       2814 if ($action eq 'delete') {
    100          
1177             ##
1178             ## Delete here
1179             ##
1180 1014 50       2562 if ($self->dryrun) {
1181 0         0 $self->_info("-- dryrun [rmtree] --> $f");
1182             }
1183             else {
1184 1014         3012 $self->_info("Deleting $f");
1185 1014         209876 File::Path::rmtree($f);
1186             }
1187             }
1188             elsif ($action eq 'backup') {
1189             ##
1190             ## Do backup as requested. Ensure:
1191             ##
1192             ## - from is the path to a file
1193             ## - to is the path to a directory of the form
1194             ## "//"
1195             ##
1196 9         1156 my $from = File::Spec->abs2rel( $f, $path );
1197 9         328 my $from_filename = File::Basename::fileparse($f);
1198 9         39 my $to = sprintf("%s/%s", $backup_path, $from);
1199              
1200 9         334 $to =~ s/$from_filename//;
1201              
1202 9         26 $from =~ s#/+#/#g; # clean multi-slashes
1203 9         192 $to =~ s#/+#/#g; #
1204              
1205 9 50       46 if ( $self->_ensure_path($to) ) {
1206             ##
1207             ## Target path now exists - now the target is expected to be a
1208             ## filename with .gz extension.
1209             ##
1210 9 100 66     56 if ( $backup_gzip && $self->{cmd_gzip} ) {
1211             ##
1212             ## Gzip in case
1213             ##
1214 7 100 66     120 if ( $from
      100        
1215             && ($from !~ /[.](gz|tgz)$/i) # do not re-gzip
1216             && (!readlink($from)) # do not gzip symlinks
1217             ){
1218 3         13 $self->_info("Gzipping $from");
1219 3         10 my $ra_cmd = [$self->{cmd_gzip}, '--force', $from ];
1220              
1221 3         9 my $cmd_txt = join(" ", @$ra_cmd);
1222 3 50       8 if ($self->dryrun) {
1223 0         0 $self->_info("-- dryrun [gzip cmd] --> $cmd_txt");
1224             }
1225             else {
1226 3         10 $self->_info("Running $cmd_txt");
1227 3         16 run3($ra_cmd);
1228             }
1229 3         13616 $from .= '.gz';
1230             }
1231             else {
1232 4         29 $self->_info("$from appears to be already gzipped");
1233             }
1234             }
1235              
1236             #
1237             # Move from -> to
1238             #
1239 9         47 my $to_file = sprintf("%s/%s", $backup_path, $from);
1240 9 50       42 if ($self->dryrun) {
1241 0         0 $self->_info("-- dryrun [mv] $from --> $to_file");
1242             }
1243             else {
1244 9         50 $self->_info("mv $from to $to_file");
1245 9 50       78 if (!move( $from, $to_file ) ){
1246 0         0 $self->_warn("Unable to move. Dying...");
1247 0         0 die sprintf("Unable to move $from to $to_file: %s", $!);
1248             }
1249             }
1250             }
1251             }
1252             }
1253            
1254 24         1697 Cwd::chdir($working_directory);
1255             }
1256              
1257             sub _ensure_path {
1258 13     13   18 my $self = shift;
1259 13         21 my $path = shift;
1260              
1261 13 100 66     378 if ( !-e $path || !-d $path ) {
1262 3         10 $self->_info("[making path] $path");
1263 3         4 eval { File::Path::mkpath($path) };
  3         310  
1264 3 50       12 $self->_warn("Unable to create $path: $@") if ($@);
1265             }
1266              
1267 13 50 33     292 if ( !-e $path || !-d $path ) {
1268 0         0 $self->_warn("Path wasn't found after trying to create it.");
1269 0         0 return 0;
1270             }
1271 13         48 return 1;
1272             }
1273              
1274             =begin _refine_plan
1275              
1276             Takes into account symlinks in the current plan.
1277              
1278             The refinement is done in the following way:
1279              
1280             1) Go through the plan, and look for symlink targets.
1281              
1282             2) Mark any symlink with as the action of it's target if it's in the cleanup
1283             directory: keep the symlink if its target is kept, delete otherwise (broken
1284             symlinks, or pointing outside the cleanup, target is being backupped...).
1285             While deciding this, build an hashref of
1286             { symlink_parent (canonical) => symlink_path (non_canonical) }.
1287              
1288             3) Add the symlink to the plan in the correct position.
1289             To do this, build another 'refined' plan.
1290             - go hrough the pathnames (visits parents first) in the plan, pop each item.
1291             - if the parent of a marked symlink is found, do the following:
1292             * mark it as 'delete' if the symlink is going to be deleted.
1293             or mark it as 'nothing' if the symlink is not going to be deleted.
1294             * push the parent in the refined plan.
1295             * push the symlink in the refined plan.
1296              
1297             4) Fix the plan to have consistent state (bubble up states between pairs of
1298             directories)
1299              
1300             Return the refined plan.
1301              
1302             =end _refine_plan
1303              
1304             =cut
1305              
1306             sub _refine_plan {
1307 24     24   50 my $self = shift;
1308 24         38 my $ra_plan = shift;
1309 24         28 my $rh_params = shift;
1310              
1311 24         65 my $rh_never_delete = $rh_params->{never_delete};
1312 24         41 my $rh_delete_once_empty = $rh_params->{delete_once_empty};
1313              
1314             # this is:
1315             # { symlink_target (canonical) =>
1316             # [ symlink_path (non canonical) ]
1317             # }
1318 24         63 my $rh_symlinks = $rh_params->{symlinks};
1319              
1320             ##
1321             ## Symlinks to delete and keep
1322             ##
1323 24         35 my %symlinks_marked; # this is:
1324             # { symlink_parent (canonical) => [
1325             # { symlink_path => symlink_path (non canonical),
1326             # action => 'delete'
1327             # }
1328             # ],...
1329             # }
1330              
1331 24         81 foreach my $ra_item (@{$ra_plan}) { # 1
  24         64  
1332 1267         1724 my ($reason, $f, $action) = @$ra_item;
1333              
1334 1267 100       2216 if (exists $rh_symlinks->{$f}) {
1335             # 2 - Keep the symlink if its target is kept, delete otherwise
1336 2         4 foreach my $sym_path (@{$rh_symlinks->{$f}}) {
  2         8  
1337              
1338 3         12 my $sym_cparent = $self->_path_check(
1339             $self->_parent_path($sym_path)
1340             );
1341              
1342 3 50       22 my $sym_action = ($action eq 'nothing') ? 'nothing' : 'delete';
1343            
1344             # two symlinks may be in the same directory,
1345 3 50       14 if (!exists $symlinks_marked{$sym_cparent}) {
1346 3         13 $symlinks_marked{$sym_cparent} = [];
1347             }
1348            
1349 3         5 push( @{$symlinks_marked{$sym_cparent}},
  3         26  
1350             { symlink_path => $sym_path,
1351             action => $sym_action
1352             }
1353             );
1354             }
1355             }
1356             }
1357            
1358             # 3
1359 24         67 my $rh_undelete_dirs = {};
1360 24         42 my $ra_refined_plan = [];
1361 24         34 while ( my $ra_item = pop @{$ra_plan} ) {
  1291         2329  
1362 1267         1586 my ($reason, $f, $action) = @$ra_item;
1363 1267 100       1749 if (!exists $symlinks_marked{$f} ) {
1364             # just re-add it
1365 1264         2721 $self->_plan_add_action( $ra_refined_plan,
1366             { action => $action,
1367             reason => $reason,
1368             f_path => $f,
1369             }
1370             );
1371             }
1372             else {
1373             # fix the action of a symlink parent - keep the parent if at least
1374             # one symlink in it is kept.
1375 3         16 my @sym_nothing =
1376 3         5 grep { $_->{action} eq 'nothing' } @{$symlinks_marked{$f}};
  3         12  
1377              
1378 3         6 my $f_action;
1379             my $f_reason;
1380 3 50       8 if (scalar @sym_nothing) { # at least one symlink to be kept
1381 3         6 $f_action = 'nothing';
1382 3         6 $f_reason = 'refined (1+ symlink kept in it)';
1383              
1384             # Propagate to the parent
1385 3         9 my $f_parent = $self->_parent_path($f);
1386 3 50       20 $rh_undelete_dirs->{ $f_parent } = 1 if $f_parent;
1387             }
1388             else {
1389 0         0 $f_action = $action;
1390 0         0 $f_reason = 'refined (all symlinks will be deleted)';
1391             }
1392             # Add the symlink parent with the updated action
1393 3         21 $self->_plan_add_action( $ra_refined_plan,
1394             { action => $f_action,
1395             reason => $f_reason,
1396             f_path => $f,
1397             }
1398             );
1399              
1400             # Add the action on each symlink's path
1401 3         7 foreach my $rh_item (@{$symlinks_marked{$f}}) {
  3         13  
1402 3         19 $self->_plan_add_action( $ra_refined_plan,
1403             { action => $rh_item->{action},
1404             reason => 'refined',
1405             f_path => $rh_item->{symlink_path},
1406             }
1407             );
1408             }
1409             }
1410             }
1411              
1412             # 4 - fix inconsistent directory state (and reverse the plan again)
1413             #
1414 24         37 my @refined_plan_fixed;
1415 24 100       55 my $add_to_head = ($rh_delete_once_empty) ? 0 : 1;
1416 24         79 while ( my $ra_item = pop @$ra_refined_plan ) {
1417 1270         1988 my ($reason, $f, $action) = @$ra_item;
1418 1270 100 100     28193 if (-d $f && !-l $f) {
1419             ##
1420             ## Directory
1421             ##
1422 374 100       781 if ($rh_undelete_dirs->{$f}) {
1423 16         25 $action = 'nothing';
1424 16         36 $reason = "bubbled (was: $reason)";
1425              
1426             # also propagate to the parent
1427 16         42 my $f_parent = $self->_parent_path($f);
1428 16 50       62 $rh_undelete_dirs->{$f_parent} = 1 if $f_parent;
1429             }
1430             }
1431             ##
1432             ## Add current item to the list
1433             ##
1434 1270         4742 $self->_plan_add_action( \@refined_plan_fixed,
1435             { action => $action,
1436             reason => $reason,
1437             f_path => $f
1438             }
1439             , $add_to_head
1440             );
1441             }
1442              
1443 24 100       580 return \@refined_plan_fixed if (!$rh_delete_once_empty);
1444              
1445 1         4 my @final_plan;
1446             my $propagate_action;
1447 1         9 while ( my $ra_item = pop @refined_plan_fixed ) {
1448 40         79 my ($reason, $f, $action) = @$ra_item;
1449             ##
1450             ## Check if we have to stop any previous propagation at this round.
1451             ##
1452 40 100       65 if ($propagate_action) {
1453              
1454 9 100       27 $propagate_action = (index($f, $propagate_action) == 0)
1455             ? $propagate_action
1456             : 0 ;
1457              
1458             }
1459              
1460             ##
1461             ## See if we should propagate the 'nothing' action to any children
1462             ##
1463 40 100 100     117 if (!$propagate_action # we are not propagating...
      100        
1464             && $self->_delete_once_empty_contains( # toplevel directory found
1465             $rh_delete_once_empty,
1466             $f
1467             )
1468             && $action eq 'nothing' ) { # ... which we don't want to delete
1469            
1470 4         8 $propagate_action = $f; # propagate until /^/
1471             # matches $f from this round
1472             }
1473              
1474 40 100 100     106 if ($propagate_action
1475             && $f ne $propagate_action ) { # aesthetics only
1476              
1477 5         7 $reason = 'all or none';
1478 5         7 $action = 'nothing';
1479             }
1480              
1481 40         154 $self->_plan_add_action( \@final_plan,
1482             { action => $action,
1483             reason => $reason,
1484             f_path => $f
1485             }
1486             );
1487             }
1488              
1489 1         50 return \@final_plan;
1490             }
1491              
1492             =for _parent_path
1493             Get the parent path of a given path. This method only accesses the disk if the
1494             f_path is found to have no parent directory (i.e., just the relative file name
1495             has been specified). In this case, we check that the current working directory
1496             contains the given file. If yes, we return the current working directory as the
1497             parent of the specified file. If not, we return undef.
1498              
1499             =cut
1500              
1501             sub _parent_path {
1502 424     424   540 my $self = shift;
1503 424         505 my $f_path = shift;
1504              
1505 424 50       799 if (!$f_path) {
1506 0         0 $self->_warn("No path was given to _parent_path()");
1507 0         0 return undef;
1508             }
1509              
1510 424         7479 my ($volume, $directories, $file) = File::Spec->splitpath($f_path);
1511              
1512             ##
1513             ## Try to reconstruct the full pathname of the parent of a relative $f_path
1514             ##
1515 424 50       1083 if (!$directories) {
1516 0         0 my $cwd = Cwd->getcwd();
1517 0 0       0 if (-e File::Spec->catpath($volume, $cwd, $file)) {
1518 0         0 $self->_info("Returning $cwd as the parent path for $file");
1519 0         0 return $cwd;
1520             }
1521             else {
1522 0         0 $self->_warn("The relative pathname $f_path was given to"
1523             . "_parent_path(), but such target doesn't exist in the current"
1524             . "working directory ($cwd)."
1525             );
1526 0         0 return undef;
1527             }
1528             }
1529              
1530 424         2602 my $f_parent = File::Spec->catpath($volume, $directories, '');
1531 424         1863 $f_parent =~ s#/$##g;
1532              
1533 424         1297 return $f_parent;
1534             }
1535              
1536              
1537             =begin _postprocess_link
1538              
1539             Given a path to a symlink and a hash reference, keep the symlink target as a
1540             key of the hash reference (canonical path), and the path to the symlink (non
1541             canonical) as the corresponding value. Because multiple symlinks can point to
1542             the same target, the value of this hashref is an arrayref of symlinks paths.
1543              
1544             Returns true on success, or false if a path to something else than a symlink is
1545             passed to this method.
1546              
1547             =end _postprocess_link
1548              
1549             =cut
1550              
1551             sub _postprocess_link {
1552 87     87   89 my $self = shift;
1553 87         76 my $rh_symlinks = shift;
1554 87         98 my $sym_path = shift;
1555              
1556 87 100       1102 if (my $sym_target = readlink($sym_path)) { # check if this is a symlink
1557 5         18 my $sym_target_cpath = $self->_path_check($sym_target);
1558 5 100       23 if (!exists $rh_symlinks->{$sym_target_cpath}) {
1559 3         11 $rh_symlinks->{$sym_target_cpath} = [];
1560             }
1561 5         7 push (@{$rh_symlinks->{$sym_target_cpath}}, $sym_path);
  5         16  
1562              
1563 5         17 return 1;
1564             }
1565              
1566             # $sym_path is not a path to a symlink
1567 82         163 return 0;
1568             }
1569              
1570             =begin _fix_pattern
1571              
1572             Refine a pattern passed from the configuration.
1573              
1574             Currently applyes the following transformation:
1575             - Remove any "/" in case the user has specified a pattern in the form of
1576             /pattern/.
1577              
1578             =end _fix_pattern
1579              
1580             =cut
1581              
1582             sub _fix_pattern {
1583 177     177   183 my $self = shift;
1584 177         200 my $pattern = shift;
1585              
1586 177 100       685 if ($pattern =~ m{^/(.*)/$}) {
1587 50         165 $pattern = $1;
1588             }
1589 177         315 return $pattern;
1590             }
1591              
1592              
1593             sub _print_never_delete {
1594 24     24   50 my $self = shift;
1595 24         40 my $rh_never_delete = shift;
1596 24 50       102 if ( !scalar keys %$rh_never_delete ) {
1597 0         0 $self->_info ("- - - [ NO NEVER DELETE FILES] - - -");
1598             }
1599             else {
1600 24         90 $self->_info ("- - - [ NEVER DELETE ] - - -");
1601 24         37 foreach my $path (keys %{$rh_never_delete->{paths}}) {
  24         140  
1602 178         489 $self->_info (sprintf("* %s", $path));
1603             }
1604 24         73 $self->_info ("");
1605             }
1606             }
1607              
1608             sub _print_delete_once_empty {
1609 1     1   3 my $self = shift;
1610 1         3 my $rh_delete_once_empty = shift;
1611 1 50       7 if ( !scalar keys %$rh_delete_once_empty ) {
1612 0         0 $self->_info ("- - - [ NO DELETE ONCE EMPTY ] - - -");
1613             }
1614             else {
1615 1         6 $self->_info ("- - - [ DELETE ONCE EMPTY ] - - -");
1616 1         3 foreach my $path (keys %{$rh_delete_once_empty->{paths}}) {
  1         11  
1617 18         49 $self->_info (sprintf("* %s", $path));
1618             }
1619 1         6 $self->_info ("");
1620             }
1621             }
1622             sub _print_plan {
1623 24     24   55 my $self = shift;
1624 24         36 my $ra_plan = shift;
1625              
1626 24         57 my $i = 1 + scalar @$ra_plan;
1627            
1628 24 50 33     176 if ( !$ra_plan || !scalar @$ra_plan ) {
1629 0         0 $self->_info ("- - - [ EMPTY PLAN ] - - -");
1630             }
1631             else {
1632 24         78 $self->_info ("- - - [ PLAN ] - - -");
1633 24         68 foreach my $ra_plan_item (@$ra_plan) {
1634 1270         928 $i--;
1635              
1636 1270         1647 my ($reason, $f, $action) = @$ra_plan_item;
1637 1270         2743 $self->_info(
1638             sprintf("%2d) [%7s] %14s - %s", $i, $action, $reason, $f)
1639             );
1640             }
1641             }
1642 24         75 $self->_info ("");
1643             }
1644              
1645             sub _info {
1646 2644     2644   2388 my $self = shift;
1647 2644         2290 my $message = shift;
1648 2644 50       3386 print " [INFO] $message\n" if $self->verbose;
1649             }
1650              
1651             sub _warn {
1652 0     0     my $self = shift;
1653 0           my $message = shift;
1654 0           warn " [WARN] $message";
1655             }
1656              
1657             sub _usage_and_exit {
1658 0     0     my $self = shift;
1659 0           my $message = shift;
1660              
1661 0           print <<"END";
1662             $0
1663             required:
1664             --conf a tasks configuration file
1665             --taskname a task from within the tasks file
1666            
1667             optional:
1668             --dryrun output plan and then exit
1669             --verbose make some noise!
1670             --help show this message
1671              
1672             For more information and documentation for how to write task config files see
1673             'perldoc File::CleanupTask'.
1674              
1675             END
1676 0 0         if ($message) {
1677 0           die( $message . "\n" );
1678             }
1679             else {
1680 0           exit;
1681             }
1682             }
1683              
1684             =head1 AUTHOR
1685              
1686             Savio Dimatteo, C<< >>
1687              
1688             =head1 BUGS
1689              
1690             Please report any bugs or feature requests to C, or through
1691             the web interface at L. I will be notified, and then you'll
1692             automatically be notified of progress on your bug as I make changes.
1693              
1694              
1695              
1696              
1697             =head1 SUPPORT
1698              
1699             You can find documentation for this module with the perldoc command.
1700              
1701             perldoc File::CleanupTask
1702              
1703              
1704             You can also look for information at:
1705              
1706             =over 4
1707              
1708             =item * RT: CPAN's request tracker (report bugs here)
1709              
1710             L
1711              
1712             =item * AnnoCPAN: Annotated CPAN documentation
1713              
1714             L
1715              
1716             =item * CPAN Ratings
1717              
1718             L
1719              
1720             =item * Search CPAN
1721              
1722             L
1723              
1724             =back
1725              
1726              
1727             =head1 ACKNOWLEDGEMENTS
1728              
1729             Thanks Alex for devising the original format of a .tasks file and offering me
1730             the opportunity to publish this work on CPAN.
1731              
1732             Thanks Mike for your feedback about canonical paths detection.
1733              
1734             Thanks David for reviewing the code.
1735              
1736             Thanks #london.pm for helping me choosing the name of this module.
1737              
1738              
1739             =head1 LICENSE AND COPYRIGHT
1740              
1741             Copyright 2012 Savio Dimatteo.
1742              
1743             This program is free software; you can redistribute it and/or modify it
1744             under the terms of either: the GNU General Public License as published
1745             by the Free Software Foundation; or the Artistic License.
1746              
1747             See http://dev.perl.org/licenses/ for more information.
1748              
1749              
1750             =cut
1751              
1752             1; # End of File::CleanupTask