File Coverage

blib/lib/Dir/Purge.pm
Criterion Covered Total %
statement 34 72 47.2
branch 13 44 29.5
condition 0 3 0.0
subroutine 4 6 66.6
pod 0 2 0.0
total 51 127 40.1


line stmt bran cond sub pod time code
1             # Dir::Purge.pm -- Purge directories
2             # RCS Info : $Id: Purge.pm,v 1.6 2006/09/19 12:24:01 jv Exp $
3             # Author : Johan Vromans
4             # Created On : Wed May 17 12:58:02 2000
5             # Last Modified By: Johan Vromans
6             # Last Modified On: Tue Sep 19 14:23:56 2006
7             # Update Count : 161
8             # Status : Unknown, Use with caution!
9              
10             # Purge directories by strategy.
11             #
12             # This is also an exercise in weird programming techniques.
13              
14             package Dir::Purge;
15              
16 1     1   981 use strict;
  1         2  
  1         42  
17 1     1   5 use Carp;
  1         1  
  1         97  
18              
19 1     1   5 use vars qw(@ISA @EXPORT @EXPORT_OK $VERSION);
  1         6  
  1         1939  
20             $VERSION = "1.02";
21             @ISA = qw(Exporter);
22             @EXPORT = qw(&purgedir);
23             @EXPORT_OK = qw(&purgedir_by_age);
24              
25             my $purge_by_age; # strategy
26              
27             sub purgedir_by_age {
28 0     0 0 0 my @dirs = @_;
29 0         0 my $opts;
30 0 0       0 if ( UNIVERSAL::isa ($dirs[0], 'HASH') ) {
31 0         0 $opts = shift (@dirs);
32 0         0 my $strat = delete $opts->{strategy};
33 0 0 0     0 if ( defined $strat && $strat ne "by_age" ) {
34 0         0 croak ("Invalid option: 'strategy'");
35             }
36 0         0 $opts->{strategy} = "by_age";
37             }
38             else {
39 0         0 $opts = { keep => shift(@dirs), strategy => "by_age" };
40             }
41 0         0 purgedir ($opts, @dirs);
42             }
43              
44              
45             # Common processing code. It verifies the arguments, directories and
46             # calls $code->(...) to do the actual purging.
47             # Nothing is done if any of the verifications fail.
48              
49             sub purgedir {
50              
51 2     2 0 2886 my (@dirs) = @_;
52 2         5 my $error = 0;
53 2         3 my $code = $purge_by_age; # default: by age
54 2         5 my $ctl = { tag => "purgedir" };
55 2         7 my @opts = qw(keep strategy reverse include verbose test debug);
56              
57             # Get the parameters. Only the 'keep' value is mandatory.
58 2 50       11 if ( UNIVERSAL::isa ($dirs[0], 'HASH') ) {
    0          
59 2         3 my $opts = shift (@dirs);
60 2         4 @{$ctl}{@opts} = delete @{$opts}{@opts};
  2         13  
  2         6  
61 2 50       8 if ( $ctl->{strategy} ) {
62 0 0       0 if ( $ctl->{strategy} eq "by_age" ) {
63 0         0 $code = $purge_by_age;
64             }
65             else {
66 0         0 carp ("Unsupported purge strategy: '$ctl->{strategy}'");
67 0         0 $error++;
68             }
69             }
70 2         9 foreach (sort keys %$opts) {
71 0         0 carp ("Unhandled option \"$_\"");
72 0         0 $error++;
73             }
74             }
75             elsif ( $dirs[0] =~ /^-?\d+$/ ) {
76 0         0 $ctl->{keep} = shift (@dirs);
77             }
78              
79 2 50       11 unless ( $ctl->{keep} ) {
    50          
80 0         0 croak ("Missing 'keep' value");
81             }
82             elsif ( $ctl->{keep} < 0 ) {
83             # Hmm. I would like to deprecate this, but on the other hand,
84             # a negative 'subscript' fits well in Perl.
85             #carp ("Negative 'keep' value is deprecated, ".
86             # "use 'reverse => 1' instead");
87 0         0 $ctl->{keep} = -$ctl->{keep};
88 0         0 $ctl->{reverse} = !$ctl->{reverse};
89             }
90              
91 2 50       6 $ctl->{verbose} = 1 unless defined ($ctl->{verbose});
92 2 50       6 $ctl->{verbose} = 9 if $ctl->{debug};
93              
94 2 50       5 if ( $ctl->{include} ) {
95 0 0       0 if ( !ref($ctl->{include}) ) {
    0          
    0          
96 0         0 croak("Invalid value for 'include': " . $ctl->{include});
97             }
98             elsif ( UNIVERSAL::isa($ctl->{include}, 'CODE') ) {
99             # OK
100             }
101             elsif ( UNIVERSAL::isa($ctl->{include}, 'Regexp') ) {
102 0         0 my $pat = $ctl->{include};
103 0     0   0 $ctl->{include} = sub { $_[0] =~ $pat };
  0         0  
104             }
105             else {
106 0         0 croak("Invalid value for 'include': " . $ctl->{include});
107             }
108             }
109              
110             # Thouroughly check the directories, and refuse to do anything
111             # in case of problems.
112 2 50       6 warn ("$ctl->{tag}: checking directories\n") if $ctl->{verbose} > 1;
113 2         5 foreach my $dir ( @dirs ) {
114             # Must be a directory.
115 2 50       31 unless ( -d $dir ) {
116 0 0       0 carp (-e _ ? "$dir: not a directory" : "$dir: not existing");
117 0         0 $error++;
118 0         0 next;
119             }
120             # We need write access since we are going to delete files.
121 2 50       11 unless ( -w _ ) {
122 0         0 carp ("$dir: no write access");
123 0         0 $error++;
124             }
125             # We need read access since we are going to get the file list.
126 2 50       8 unless ( -r _ ) {
127 0         0 carp ("$dir: no read access");
128 0         0 $error++;
129             }
130             # Probably need this as well, don't know.
131 2 50       10 unless ( -x _ ) {
132 0         0 carp ("$dir: no access");
133 0         0 $error++;
134             }
135             }
136              
137             # If errors, bail out unless testing.
138 2 50       6 if ( $error ) {
139 0 0       0 if ( $ctl->{test} ) {
140 0         0 carp ("$ctl->{tag}: errors detected, continuing");
141             }
142             else {
143 0         0 croak ("$ctl->{tag}: errors detected, nothing done");
144             }
145             }
146              
147             # Process the directories.
148 2         4 foreach my $dir ( @dirs ) {
149 2         5 $code->($ctl, $dir);
150             }
151             };
152              
153             # Everything else is assumed to be small building-block routines to
154             # implement a plethora of purge strategies.
155             # Actually, I cannot think of any right now.
156              
157             # Gather file names and additional info.
158             my $gather = sub {
159             my ($ctl, $dir, $what) = @_;
160              
161             local (*DIR);
162             opendir (DIR, $dir)
163             or croak ("dir: $!"); # shouldn't happen -- we've checked!
164             my @files;
165             foreach ( readdir (DIR) ) {
166             next if $ctl->{include} && !$ctl->{include}->($_, $dir);
167             next if /^\./;
168             next unless -f "$dir/$_";
169             push (@files, [ "$dir/$_", $what->("$dir/$_") ]);
170             }
171             closedir (DIR);
172              
173             warn ("$ctl->{tag}: $dir: ", scalar(@files), " files\n")
174             if $ctl->{verbose} > 1;
175             warn ("$ctl->{tag}: $dir: @{[map { $_->[0] } @files]}\n")
176             if $ctl->{debug};
177              
178             \@files;
179             };
180              
181             # Sort the list on the supplied info.
182             my $sort = sub {
183             my ($ctl, $files) = @_;
184              
185             my @sorted = map { $_->[0] } sort { $a->[1] <=> $b->[1] } @$files;
186             warn ("$ctl->{tag}: sorted: @sorted\n") if $ctl->{debug};
187             \@sorted;
188             };
189              
190             # Remove the files to keep from the list.
191             my $reduce = sub {
192             my ($ctl, $files) = @_;
193              
194             if ( $ctl->{reverse} ) {
195             # Keep the newest files (tail of the list).
196             splice (@$files, @$files-$ctl->{keep}, $ctl->{keep});
197             }
198             else {
199             # Keep the oldest files (head of the list).
200             splice (@$files, 0, $ctl->{keep});
201             }
202             $files;
203             };
204              
205             # Remove the files in the list.
206             my $purge = sub {
207             my ($ctl, $files) = @_;
208              
209             # Remove the selected files.
210             foreach ( @$files ) {
211             if ( $ctl->{test} ) {
212             warn ("$ctl->{tag}: candidate: $_\n");
213             }
214             else {
215             warn ("$ctl->{tag}: removing $_\n") if $ctl->{verbose};
216             unlink ($_) or carp ("$_: $!");
217             }
218             }
219             };
220              
221             # Processing routine: purge by file age.
222             $purge_by_age = sub {
223             my ($ctl, $dir) = @_;
224              
225             warn ("$ctl->{tag}: purging directory $dir (by age, keep $ctl->{keep})\n")
226             if $ctl->{verbose} > 1;
227              
228             # Gather, with age info.
229             my $files = $gather->($ctl, $dir, sub { -M _ });
230              
231             # Is there anything to do?
232             if ( @$files <= $ctl->{keep} ) {
233             warn ("$ctl->{tag}: $dir: below limit\n") if $ctl->{verbose} > 1;
234             return;
235             }
236              
237             # Sort, reduce and purge.
238             $purge->($ctl, $reduce->($ctl, $sort->($ctl, $files)));
239             };
240              
241             1;
242              
243             __END__