File Coverage

blib/lib/File/Write/Rotate.pm
Criterion Covered Total %
statement 214 232 92.2
branch 96 130 73.8
condition 33 43 76.7
subroutine 21 22 95.4
pod 7 7 100.0
total 371 434 85.4


line stmt bran cond sub pod time code
1             package File::Write::Rotate;
2              
3             our $DATE = '2016-10-07'; # DATE
4             our $VERSION = '0.31'; # VERSION
5              
6 3     3   96873 use 5.010001;
  3         8  
7 3     3   9 use strict;
  3         3  
  3         46  
8 3     3   9 use warnings;
  3         3  
  3         73  
9              
10             # we must not use Log::Any, looping if we are used as log output
11             #use Log::Any '$log';
12              
13 3     3   10 use File::Spec;
  3         4  
  3         50  
14 3     3   1642 use IO::Compress::Gzip qw(gzip $GzipError);
  3         83293  
  3         351  
15 3     3   21 use Scalar::Util qw(weaken);
  3         5  
  3         138  
16             #use Taint::Runtime qw(untaint is_tainted);
17 3     3   1669 use Time::HiRes 'time';
  3         3621  
  3         11  
18              
19             our $Debug;
20              
21             sub new {
22 278     278 1 40359 my $class = shift;
23 278         799 my %args0 = @_;
24              
25 278         279 my %args;
26              
27             defined($args{dir} = delete $args0{dir})
28 278 50       962 or die "Please specify dir";
29             defined($args{prefix} = delete $args0{prefix})
30 278 50       577 or die "Please specify prefix";
31 278   100     898 $args{suffix} = delete($args0{suffix}) // "";
32              
33 278   100     618 $args{size} = delete($args0{size}) // 0;
34              
35 278         259 $args{period} = delete($args0{period});
36 278 100       548 if ($args{period}) {
37 39 50       204 $args{period} =~ /\A(daily|day|month|monthly|year|yearly)\z/
38             or die "Invalid period, please use daily/monthly/yearly";
39             }
40              
41 278         392 for (map {"hook_$_"} qw(before_rotate after_rotate after_create
  1390         1893  
42             before_write a)) {
43 1390 100       1860 next unless $args0{$_};
44 17         23 $args{$_} = delete($args0{$_});
45             die "Invalid $_, please supply a coderef"
46 17 50       37 unless ref($args{$_}) eq 'CODE';
47             }
48              
49 278 100 66     755 if (!$args{period} && !$args{size}) {
50 21         26 $args{size} = 10 * 1024 * 1024;
51             }
52              
53 278   100     720 $args{histories} = delete($args0{histories}) // 10;
54              
55 278         263 $args{binmode} = delete($args0{binmode});
56              
57 278         419 $args{buffer_size} = delete($args0{buffer_size});
58              
59 278   100     730 $args{lock_mode} = delete($args0{lock_mode}) // 'write';
60 278 50       998 $args{lock_mode} =~ /\A(none|write|exclusive)\z/
61             or die "Invalid lock_mode, please use none/write/exclusive";
62              
63 278         252 $args{rotate_probability} = delete($args0{rotate_probability});
64 278 50       454 if (defined $args{rotate_probability}) {
65 0 0 0     0 $args{rotate_probability} > 0 && $args{rotate_probability} < 1.0
66             or die "Invalid rotate_probability, must be 0 < x < 1";
67             }
68              
69 278 50       426 if (keys %args0) {
70 0         0 die "Unknown arguments to new(): ".join(", ", sort keys %args0);
71             }
72              
73 278         332 $args{_buffer} = [];
74              
75 278         388 my $self = bless \%args, $class;
76              
77             $self->{_exclusive_lock} = $self->_get_lock
78 278 100       479 if $self->{lock_mode} eq 'exclusive';
79              
80 278         554 $self;
81             }
82              
83             sub buffer_size {
84 2     2 1 6 my $self = shift;
85 2 100       5 if (@_) {
86 1         1 my $old = $self->{buffer_size};
87 1         2 $self->{buffer_size} = $_[0];
88 1         1 return $old;
89             } else {
90 1         3 return $self->{buffer_size};
91             }
92             }
93              
94             sub handle {
95 1     1 1 5 my $self = shift;
96 1         2 $self->{_fh};
97             }
98              
99             sub path {
100 0     0 1 0 my $self = shift;
101 0         0 $self->{_fp};
102             }
103              
104             # file path, without the rotate suffix
105             sub _file_path {
106 801     801   2297 my ($self) = @_;
107              
108             # _now is calculated every time we access this method
109 801         1374 $self->{_now} = time();
110              
111 801         3548 my @lt = localtime($self->{_now});
112 801         1174 $lt[5] += 1900;
113 801         507 $lt[4]++;
114              
115 801         541 my $period;
116 801 100       1029 if ($self->{period}) {
117 110 100       618 if ($self->{period} =~ /year/i) {
    100          
    50          
118 13         40 $period = sprintf("%04d", $lt[5]);
119             } elsif ($self->{period} =~ /month/) {
120 13         46 $period = sprintf("%04d-%02d", $lt[5], $lt[4]);
121             } elsif ($self->{period} =~ /day|daily/) {
122 84         300 $period = sprintf("%04d-%02d-%02d", $lt[5], $lt[4], $lt[3]);
123             }
124             } else {
125 691         669 $period = "";
126             }
127              
128             my $path = join(
129             '',
130             $self->{dir}, '/',
131             $self->{prefix},
132             length($period) ? ".$period" : "",
133             $self->{suffix},
134 801 100       1822 );
135 801 100       890 if (wantarray) {
136 308         642 return ($path, $period);
137             } else {
138 493         899 return $path;
139             }
140             }
141              
142             sub lock_file_path {
143 996     996 1 1318 my ($self) = @_;
144 996         16219 return File::Spec->catfile($self->{dir}, $self->{prefix} . '.lck');
145             }
146              
147             sub _get_lock {
148 592     592   546 my ($self) = @_;
149 592 100       1026 return undef if $self->{lock_mode} eq 'none';
150 588 100       975 return $self->{_weak_lock} if defined($self->{_weak_lock});
151              
152 510         4199 require File::Flock::Retry;
153 510         2883 my $lock = File::Flock::Retry->lock($self->lock_file_path);
154 510         45463 $self->{_weak_lock} = $lock;
155 510         1238 weaken $self->{_weak_lock};
156 510         555 return $lock;
157             }
158              
159             # will return \@files. each entry is [filename without compress suffix,
160             # rotate_suffix (for sorting), period (for sorting), compress suffix (for
161             # renaming back)]
162             sub _get_files {
163 98     98   87 my ($self) = @_;
164              
165 98 50       1761 opendir my ($dh), $self->{dir} or do {
166 0         0 warn "Can't opendir '$self->{dir}': $!";
167 0         0 return;
168             };
169              
170 98         90 my @files;
171 98         1480 while (my $e = readdir($dh)) {
172 463 100       721 my $cs = $1 if $e =~ s/(\.gz)\z//; # compress suffix
173 463 100       3027 next unless $e =~ /\A\Q$self->{prefix}\E
174             (?:\. (?\d{4}(?:-\d\d(?:-\d\d)?)?) )?
175             \Q$self->{suffix}\E
176             (?:\. (?\d+) )?
177             \z
178             /x;
179             push @files,
180 3   100 3   4615 [ $e, $+{rotate_suffix} // 0, $+{period} // "", $cs // "" ];
  3   100     1029  
  3   100     4133  
  152         2296  
181             }
182 98         540 closedir($dh);
183              
184 98 50       415 [ sort { $a->[2] cmp $b->[2] || $b->[1] <=> $a->[1] } @files ];
  127         433  
185             }
186              
187             # rename (increase rotation suffix) and keep only n histories. note: failure in
188             # rotating should not be fatal, we just warn and return.
189             sub _rotate_and_delete {
190 85     85   6524 my ($self, %opts) = @_;
191              
192 85         85 my $delete_only = $opts{delete_only};
193 85         149 my $lock = $self->_get_lock;
194             CASE:
195             {
196 85 50       87 my $files = $self->_get_files or last CASE;
  85         128  
197              
198             # is there a compression process in progress? this is marked by the
199             # existence of -compress.pid PID file.
200             #
201             # XXX check validity of PID file, otherwise a stale PID file will always
202             # prevent rotation to be done
203 85 50       872 if (-f "$self->{dir}/$self->{prefix}-compress.pid") {
204 0         0 warn "Compression is in progress, rotation is postponed";
205 0         0 last CASE;
206             }
207              
208 7         22 $self->{hook_before_rotate}->($self, [map {$_->[0]} @$files])
209 85 100       182 if $self->{hook_before_rotate};
210              
211 85         1898 my @deleted;
212             my @renamed;
213              
214 0         0 my $i;
215 85         105 my $dir = $self->{dir};
216 85 100       172 my $rotating_period = @$files ? $files->[-1][2] : undef;
217 85         146 for my $f (@$files) {
218 122         279 my ($orig, $rs, $period, $cs) = @$f;
219 122         139 $i++;
220              
221             #say "DEBUG: is_tainted \$dir? ".is_tainted($dir);
222             #say "DEBUG: is_tainted \$orig? ".is_tainted($orig);
223             #say "DEBUG: is_tainted \$cs? ".is_tainted($cs);
224              
225             # TODO actually, it's more proper to taint near the source (in this
226             # case, _get_files)
227             #untaint \$orig;
228 122         350 ($orig) = $orig =~ /(.*)/s; # we use this instead, no module needed
229              
230 122 100       259 if ($i <= @$files - $self->{histories}) {
231 22 50       42 say "DEBUG: Deleting old rotated file $dir/$orig$cs ..."
232             if $Debug;
233 22 50       995 if (unlink "$dir/$orig$cs") {
234 22         60 push @deleted, "$orig$cs";
235             } else {
236 0         0 warn "Can't delete $dir/$orig$cs: $!";
237             }
238 22         46 next;
239             }
240 100 100 66     413 if (!$delete_only && defined($rotating_period) && $period eq $rotating_period) {
      100        
241 48         55 my $new = $orig;
242 48 100       67 if ($rs) {
243 14         73 $new =~ s/\.(\d+)\z/"." . ($1+1)/e;
  14         54  
244             } else {
245 34         45 $new .= ".1";
246             }
247 48 50       97 if ($new ne $orig) {
248 48 50       77 say "DEBUG: Renaming rotated file $dir/$orig$cs -> ".
249             "$dir/$new$cs ..." if $Debug;
250 48 50       1752 if (rename "$dir/$orig$cs", "$dir/$new$cs") {
251 48         172 push @renamed, "$new$cs";
252             } else {
253 0         0 warn "Can't rename '$dir/$orig$cs' -> '$dir/$new$cs': $!";
254             }
255             }
256             }
257             }
258              
259             $self->{hook_after_rotate}->($self, \@renamed, \@deleted)
260 85 100       363 if $self->{hook_after_rotate};
261             } # CASE
262             }
263              
264             sub _open {
265 298     298   238 my $self = shift;
266              
267 298         333 my ($fp, $period) = $self->_file_path;
268 298 50       8186 open $self->{_fh}, ">>", $fp or die "Can't open '$fp': $!";
269 298 100       532 if (defined $self->{binmode}) {
270             binmode $self->{_fh}, $self->{binmode}
271 2 50       11 or die "Can't set PerlIO layer on '$fp': $!";
272             }
273 298         662 my $oldfh = select $self->{_fh};
274 298         509 $| = 1;
275 298         471 select $oldfh; # set autoflush
276 298         392 $self->{_fp} = $fp;
277 298 100       792 $self->{hook_after_create}->($self) if $self->{hook_after_create};
278             }
279              
280             # (re)open file and optionally rotate if necessary
281             sub _rotate_and_open {
282              
283 493     493   409 my $self = shift;
284 493         394 my ($do_open, $do_rotate) = @_;
285 493         365 my $fp;
286             my %rotate_params;
287              
288             CASE:
289             {
290             # if instructed, only do rotate some of the time to shave overhead
291 493 0 33     330 if ($self->{rotate_probability} && $self->{_fh}) {
  493         872  
292 0 0       0 last CASE if rand() > $self->{rotate_probability};
293             }
294              
295 493         615 $fp = $self->_file_path;
296 493 100       4952 unless (-e $fp) {
297 50         50 $do_open++;
298 50         39 $do_rotate++;
299 50         82 $rotate_params{delete_only} = 1;
300 50         63 last CASE;
301             }
302              
303             # file is not opened yet, open
304 443 100       740 unless ($self->{_fh}) {
305 219         352 $self->_open;
306             }
307              
308             # period has changed, rotate
309 443 50       711 if ($self->{_fp} ne $fp) {
310 0         0 $do_rotate++;
311 0         0 $rotate_params{delete_only} = 1;
312 0         0 last CASE;
313             }
314              
315             # check whether size has been exceeded
316 443         368 my $inode;
317              
318 443 100       693 if ($self->{size} > 0) {
319              
320 437         1870 my @st = stat($self->{_fh});
321 437         483 my $size = $st[7];
322 437         314 $inode = $st[1];
323              
324 437 100       591 if ($size >= $self->{size}) {
325 28 50       52 say "DEBUG: Size of $self->{_fp} is $size, exceeds $self->{size}, rotating ..."
326             if $Debug;
327 28         29 $do_rotate++;
328 28         52 last CASE;
329             } else {
330             # stat the current file (not our handle _fp)
331 409         3104 my @st = stat($fp);
332 409 50       678 die "Can't stat '$fp': $!" unless @st;
333 409         296 my $finode = $st[1];
334              
335             # check whether other process has rename/rotate under us (for
336             # example, 'prefix' has been moved to 'prefix.1'), in which case
337             # we need to reopen
338 409 100 66     1725 if (defined($inode) && $finode != $inode) {
339 1         3 $do_open++;
340             }
341             }
342              
343             }
344             } # CASE
345              
346 493 100       837 $self->_rotate_and_delete(%rotate_params) if $do_rotate;
347 493 100 100     2959 $self->_open if $do_rotate || $do_open; # (re)open
348             }
349              
350             sub write {
351 493     493 1 61069 my $self = shift;
352              
353             # the buffering implementation is currently pretty naive. it assume any
354             # die() as a write failure and store the message to buffer.
355              
356             # FYI: if privilege is dropped from superuser, the failure is usually at
357             # locking the lock file (permission denied).
358              
359 493         407 my @msg = (map( {@$_} @{ $self->{_buffer} } ), @_);
  6         12  
  493         1152  
360              
361 493         522 eval {
362 493         899 my $lock = $self->_get_lock;
363              
364 493         769 $self->_rotate_and_open;
365              
366             $self->{hook_before_write}->($self, \@msg, $self->{_fh})
367 493 100       2035 if $self->{hook_before_write};
368              
369 488         1414 print { $self->{_fh} } @msg;
  488         7011  
370 488         1961 $self->{_buffer} = [];
371              
372             };
373 493         27817 my $err = $@;
374              
375 493 100       1307 if ($err) {
376 5 100 50     11 if (($self->{buffer_size} // 0) > @{ $self->{_buffer} }) {
  5         11  
377             # put message to buffer temporarily
378 4         5 push @{ $self->{_buffer} }, [@_];
  4         22  
379             } else {
380             # buffer is already full, let's dump the buffered + current message
381             # to the die message anyway.
382             die join(
383             "",
384             "Can't write",
385             (
386 1         4 @{ $self->{_buffer} }
387             ? " (buffer is full, "
388 1 50       2 . scalar(@{ $self->{_buffer} })
  1         10  
389             . " message(s))"
390             : ""
391             ),
392             ": $err, message(s)=",
393             @msg
394             );
395             }
396             }
397             }
398              
399             sub compress {
400 13     13 1 2127 my ($self) = shift;
401              
402 13         29 my $lock = $self->_get_lock;
403 13         29 my $files_ref = $self->_get_files;
404 13         14 my $done_compression = 0;
405              
406 13 50       11 if (@{$files_ref}) {
  13         29  
407 13         629 require Proc::PID::File;
408              
409             my $pid = Proc::PID::File->new(
410             dir => $self->{dir},
411 13         1744 name => "$self->{prefix}-compress",
412             verify => 1,
413             );
414 13         358 my $latest_period = $files_ref->[-1][2];
415              
416 13 50       38 if ($pid->alive) {
417 0         0 warn "Another compression is in progress";
418             } else {
419 13         1286 my @tocompress;
420             #use DD; dd $self;
421 13         14 for my $file_ref (@{$files_ref}) {
  13         32  
422 30         22 my ($orig, $rs, $period, $cs) = @{ $file_ref };
  30         61  
423             #say "D:compress: orig=<$orig> rs=<$rs> period=<$period> cs=<$cs>";
424 30 50       49 next if $cs; # already compressed
425 30 100 66     67 next if !$self->{period} && !$rs; # not old file
426 25 100 100     87 next if $self->{period} && $period eq $latest_period; # not old file
427 17         152 push @tocompress, File::Spec->catfile($self->{dir}, $orig);
428             }
429              
430 13 50       53 if (@tocompress) {
431 13         19 for my $file (@tocompress) {
432             gzip($file => "$file.gz")
433 17 50       72 or do { warn "gzip failed: $GzipError\n"; next };
  0         0  
  0         0  
434 17         26037 unlink $file;
435             }
436 13         67 $done_compression = 1;
437             }
438             }
439             }
440              
441 13         765 return $done_compression;
442              
443             }
444              
445             sub DESTROY {
446 278     278   63432 my ($self) = @_;
447              
448             # Proc::PID::File's DESTROY seem to create an empty PID file, remove it.
449 278         6170 unlink "$self->{dir}/$self->{prefix}-compress.pid";
450             }
451              
452             1;
453              
454             # ABSTRACT: Write to files that archive/rotate themselves
455              
456             __END__