File Coverage

blib/lib/File/CleanupTask.pm
Criterion Covered Total %
statement 450 518 86.8
branch 146 204 71.5
condition 51 72 70.8
subroutine 43 46 93.4
pod 6 6 100.0
total 696 846 82.2


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