File Coverage

blib/lib/File/Rotate/Simple.pm
Criterion Covered Total %
statement 96 104 92.3
branch 30 40 75.0
condition 14 15 93.3
subroutine 15 15 100.0
pod 2 2 100.0
total 157 176 89.2


line stmt bran cond sub pod time code
1             package File::Rotate::Simple;
2              
3 4     4   5284 use Moo 1.001000;
  4         46475  
  4         27  
4             extends 'Exporter';
5              
6 4     4   9396 use Graph;
  4         431368  
  4         224  
7 4     4   41 use List::Util 1.43, qw/ first /;
  4         11  
  4         648  
8 4     4   30 use Module::Runtime qw/ require_module /;
  4         11  
  4         44  
9 4     4   308 use Path::Tiny 0.015;
  4         77  
  4         219  
10 4     4   2132 use Ref::Util qw/ is_blessed_ref /;
  4         6531  
  4         352  
11 4     4   983 use Time::Seconds qw/ ONE_DAY /;
  4         2790  
  4         290  
12 4     4   2308 use Types::Standard -types;
  4         304040  
  4         51  
13              
14 4     4   21573 use namespace::autoclean;
  4         58226  
  4         21  
15              
16             our $VERSION = 'v0.2.4';
17              
18             # ABSTRACT: no-frills file rotation
19              
20             # RECOMMEND PREREQ: Class::Load::XS
21             # RECOMMEND PREREQ: Ref::Util::XS
22             # RECOMMEND PREREQ: Type::Tiny::XS
23              
24             our @EXPORT_OK = qw/ rotate_files /;
25              
26              
27             has age => (
28             is => 'ro',
29             isa => Int,
30             default => 0,
31             );
32              
33              
34             has max => (
35             is => 'ro',
36             isa => Int,
37             default => 0,
38             );
39              
40              
41             has file => (
42             is => 'ro',
43             isa => InstanceOf['Path::Tiny'],
44             coerce => sub { path(shift) },
45             required => 1,
46             );
47              
48              
49             has start_num => (
50             is => 'ro',
51             isa => Int,
52             default => 1,
53             );
54              
55              
56             has extension_format => (
57             is => 'ro',
58             isa => Str,
59             default => '.%#',
60             );
61              
62              
63             has replace_extension => (
64             is => 'ro',
65             isa => Maybe[Str],
66             );
67              
68              
69             has if_missing => (
70             is => 'ro',
71             isa => Bool,
72             default => 1,
73             );
74              
75              
76             has touch => (
77             is => 'ro',
78             isa => Bool,
79             default => 0,
80             );
81              
82              
83             has time => (
84             is => 'rw',
85             isa => InstanceOf[qw/ Time::Piece Time::Moment DateTime /],
86             lazy => 1,
87             default => sub { require_module('Time::Piece'); Time::Piece::localtime() },
88             handles => {
89             _strftime => 'strftime',
90             _epoch => 'epoch',
91             },
92             );
93              
94              
95             sub rotate {
96 10     10 1 31544 my $self = shift;
97              
98 10 100       45 unless (is_blessed_ref $self) {
99 5 50       35 my %args = (@_ == 1) ? %{ $_[0] } : @_;
  0         0  
100              
101 5 50       19 if (my $files = delete $args{files}) {
102 0         0 foreach my $file (@{$files}) {
  0         0  
103 0         0 $self->new( %args, file => $file )->rotate;
104             }
105 0         0 return;
106             }
107              
108 5         136 $self = $self->new(%args);
109             }
110              
111 10         722 my $max = $self->max;
112 10 100       55 my $age = ($self->age)
113             ? $self->_epoch - ($self->age * ONE_DAY)
114             : 0;
115              
116 10         253 my @files = @{ $self->_build_files_to_rotate };
  10         29  
117              
118 10         2285 my $index = scalar( @files );
119              
120 10         36 while ($index--) {
121              
122 21 50       502 my $file = $files[$index] or next;
123              
124 21         42 my $current = $file->{current};
125 21         35 my $rotated = $file->{rotated};
126              
127 21 100       56 unless (defined $rotated) {
128 1         7 $current->remove;
129 1         79 next;
130             }
131              
132 20 50 66     61 if ($max && $index >= $max) {
133 0         0 $current->remove;
134 0         0 next;
135             }
136              
137 20 100 100     62 if ($age && $current->stat->mtime < $age) {
138 1         165 $current->remove;
139 1         65 next;
140             }
141              
142 19 50       8638 die "Cannot move ${current} -> ${rotated}: file exists"
143             if $rotated->exists;
144              
145 19         387 $current->move($rotated);
146             }
147              
148 10 50       388 $self->file->touch if $self->touch;
149              
150             # TODO: chmod/chown arguments
151             }
152              
153              
154             sub _build_files_to_rotate {
155 10     10   24 my ($self) = @_;
156              
157 10         19 my %files;
158              
159 10         26 my $num = $self->start_num;
160              
161 10         28 my $file = $self->_rotated_name( $num );
162 10 100       312 if ($self->file->exists) {
163              
164 3         102 $files{ $self->file } = {
165             current => $self->file,
166             rotated => $file,
167             };
168              
169             } else {
170              
171 7 50       143 return [ ] unless $self->if_missing;
172              
173             }
174              
175 10         52 my $max = $self->max;
176 10   100     36 while ($file->exists || ($max && $num <= $max)) {
      100        
177              
178 19         333 my $rotated = $self->_rotated_name( ++$num );
179              
180 19 50       541 last if $rotated eq $file;
181              
182 19 100       148 if ($file->exists) {
183 18 100 100     418 $files{ $file } = {
184             current => $file,
185             rotated => (!$max || $num <= $max) ? $rotated : undef,
186             };
187             }
188              
189 19         124 $file = $rotated;
190              
191             }
192              
193             # Using a topoligical sort is probably overkill, but it allows us
194             # to use more complicated filename rotation schemes in a subclass
195             # without having to worry about file order.
196              
197 10         269 my $g = Graph->new;
198 10         2433 foreach my $file (values %files) {
199 21         1308 my $current = $file->{current};
200 21 100       83 if (my $rotated = $file->{rotated}) {
201 20         51 $g->add_edge( $current->stringify,
202             $rotated->stringify );
203             } else {
204 1         3 $g->add_vertex( $current->stringify );
205             }
206             }
207              
208             # Now check that there is not more than one file being rotated to
209             # the same name.
210              
211 10         778 my %rotated;
212 10         42 $rotated{$_->[1]}++ for ($g->edges);
213              
214 10 50   20   982 if (my $duplicate = first { $rotated{$_} > 1 } keys %rotated) {
  20         50  
215 0         0 die "multiple files are rotated to '${duplicate}'";
216             }
217              
218 10 50       57 die "dependency chain is cyclic"
219             if $g->has_a_cycle;
220              
221             return [
222 27         112 grep { defined $_ }
223 10         11529 map { $files{$_} } $g->topological_sort()
  27         18284  
224             ];
225              
226             }
227              
228              
229             sub _rotated_name {
230 136     136   40017 my ($self, $index) = @_;
231              
232 136         355 my $format = $self->extension_format;
233             {
234 4     4   4340 no warnings 'uninitialized';
  4         11  
  4         1212  
  136         203  
235 136         881 $format =~ s/\%(\d+)*#/sprintf("\%0$1d", $index)/ge;
  133         755  
236             }
237              
238 136         500 my $file = $self->file->stringify;
239 136 100       778 my $extension = ($format =~ /\%/) ? $self->_strftime($format) : $format;
240 136         605 my $replace = $self->replace_extension;
241              
242 136 100       263 if (defined $replace) {
243              
244 4         9 my $re = quotemeta($replace);
245 4         33 $file =~ s/${re}$/${extension}/;
246              
247 4         14 return path($file);
248              
249             } else {
250              
251 132         429 return path( $file . $extension );
252              
253             }
254             }
255              
256              
257             sub rotate_files {
258 1     1 1 21475 __PACKAGE__->rotate( @_ );
259             }
260              
261              
262             1;
263              
264             __END__