File Coverage

blib/lib/Dir/Write/Rotate.pm
Criterion Covered Total %
statement 114 123 92.6
branch 50 66 75.7
condition 11 15 73.3
subroutine 8 8 100.0
pod 3 3 100.0
total 186 215 86.5


line stmt bran cond sub pod time code
1             package Dir::Write::Rotate;
2              
3             our $DATE = '2017-06-26'; # DATE
4             our $VERSION = '0.003'; # VERSION
5              
6 2     2   90449 use strict;
  2         7  
  2         61  
7 2     2   12 use warnings;
  2         5  
  2         75  
8              
9 2     2   12 use Fcntl qw(:DEFAULT);
  2         6  
  2         3289  
10              
11             sub new {
12 8     8 1 17554 my ($pkg, %args) = @_;
13              
14 8         17 my $self = {};
15 8 100       32 if (defined(my $v = delete $args{path})) {
16 7         19 $self->{path} = $v;
17             } else {
18 1         11 die "Please specify path";
19             }
20 7 100       26 if (defined(my $v = delete $args{filename_pattern})) {
21 1         3 $self->{filename_pattern} = $v;
22             } else {
23 6         14 $self->{filename_pattern} = '%Y-%m-%d-%H%M%S.pid-%{pid}.%{ext}';
24             }
25 7 100       24 if (defined(my $v = delete $args{filename_sub})) {
26 3         6 $self->{filename_sub} = $v;
27             }
28 7 100       25 if (defined(my $v = delete $args{max_size})) {
29 1         3 $self->{max_size} = $v;
30             }
31 7 100       20 if (defined(my $v = delete $args{max_files})) {
32 1         3 $self->{max_files} = $v;
33             }
34 7 100       23 if (defined(my $v = delete $args{max_age})) {
35 1         2 $self->{max_age} = $v;
36             }
37 7 100       21 if (defined(my $v = delete $args{rotate_probability})) {
38 3         7 $self->{rotate_probability} = $v;
39             } else {
40 4         7 $self->{rotate_probability} = 0.25;
41             }
42 7 100       27 if (keys %args) {
43 1         13 die "Unknown argument(s): ".join(", ", sort keys %args);
44             }
45 6         21 bless $self, $pkg;
46             }
47              
48             my $default_ext = 'log';
49             my $libmagic;
50             sub _resolve_pattern {
51 21     21   44 my ($self, $content) = @_;
52              
53 21 100       71 if ($self->{filename_sub}) {
54 12         40 return $self->{filename_sub}($self, $content);
55             }
56              
57 9         1097 require POSIX;
58              
59 9         11616 my $pat = $self->{filename_pattern};
60 9         28 my $now = time;
61              
62 9         39 my @vars = qw(Y y m d H M S z Z %);
63 9         24 my $strftime = POSIX::strftime(join("|", map {"%$_"} @vars),
  90         1010  
64             localtime($now));
65 9         43 my %vars;
66 9         19 my $i = 0;
67 9         52 for (split /\|/, $strftime) {
68 90         225 $vars{ $vars[$i] } = $_;
69 90         154 $i++;
70             }
71              
72 9         28 push @vars, "{pid}";
73 9         33 $vars{"{pid}"} = $$;
74              
75 9         22 push @vars, "{ext}";
76             $vars{"{ext}"} = sub {
77 7 100   7   27 unless (defined $libmagic) {
78 2 50       6 if (eval { require File::LibMagic;
  2         1183  
79 0         0 require Media::Type::Simple; 1 }) {
  0         0  
80 0         0 $libmagic = File::LibMagic->new;
81             } else {
82 2         11 $libmagic = 0;
83             }
84             }
85 7 50       44 return $default_ext unless $libmagic;
86 0         0 my $type = $libmagic->checktype_contents($content);
87 0 0       0 return $default_ext unless $type;
88 0         0 $type =~ s/[; ].*//; # only get the mime type
89 0         0 my $ext = Media::Type::Simple::ext_from_type($type);
90 0 0       0 ($ext) = $ext =~ /(.+)/ if $ext; # untaint
91 0   0     0 return $ext || $default_ext;
92 9         60 };
93              
94 9         23 my $res = $pat;
95 9         54 $res =~ s[%(\{\w+\}|\S)]
96             [defined($vars{$1}) ?
97             ( ref($vars{$1}) eq 'CODE' ?
98 56 100       305 $vars{$1}->() : $vars{$1} ) :
    50          
99             die("Invalid format in filename_pattern `%$1'")]eg;
100 9         100 $res;
101             }
102              
103             sub write {
104 21     21 1 2000260 my ($self, $content) = @_;
105              
106 21         65 my $filename0 = $self->_resolve_pattern($content);
107              
108 21         101 my $filename = "$self->{path}/$filename0";
109 21         44 my $i = 0;
110 21         36 my $fh;
111 21         47 while (1) {
112 33 100       578 if (-e $filename) {
113 12         28 $i++;
114 12         35 $filename = "$self->{path}/$filename0.$i";
115 12         26 next;
116             }
117             # to avoid race condition
118 21 50       1093 sysopen($fh, $filename, O_WRONLY|O_CREAT|O_EXCL)
119             or die "Can't open $filename: $!";
120 21         73 last;
121             }
122 21 50       146 print $fh $content or die "Can't print to $filename: $!";
123 21 50       593 close $fh or die "Can't write to $filename: $!";
124 21 100       150 $self->rotate if (rand() < $self->{rotate_probability});
125             }
126              
127             sub rotate {
128 15     15 1 34 my $self = shift;
129              
130 15         28 my $ms = $self->{max_size};
131 15         29 my $mf = $self->{max_files};
132 15         24 my $ma = $self->{max_age};
133              
134 15 50 100     89 return unless (defined($ms) || defined($mf) || defined($ma));
      66        
135              
136 15         37 my @entries;
137 15         28 my $now = time;
138 15         31 my $path = $self->{path};
139 15 50       322 opendir my $dh, $path or die "Can't open dir $path: $!";
140 15         289 while (my $e = readdir $dh) {
141 74         309 ($e) = $e =~ /(.*)/s; # untaint
142 74 100 100     432 next if $e eq '.' || $e eq '..';
143 44         525 my @st = stat "$path/$e";
144 44         417 push @entries, {name => $e, age => ($now-$st[10]), size => $st[7]};
145             }
146 15         100 closedir $dh;
147              
148             @entries = sort {
149 15         69 $a->{age} <=> $b->{age} ||
150             $b->{name} cmp $a->{name}
151 42 50       135 } @entries;
152              
153             # max files
154 15 100 100     58 if (defined($mf) && @entries > $mf) {
155 2         8 for (splice @entries, $mf) {
156 2         7 my $fpath = "$path/$_->{name}";
157 2 50       84 unlink $fpath or warn "Can't unlink $fpath: $!";
158             }
159             }
160              
161             # max age
162 15 100       46 if (defined($ma)) {
163 5         10 my $i = 0;
164 5         13 for (@entries) {
165 12 100       68 if ($_->{age} > $ma) {
166 1         8 for (splice @entries, $i) {
167 4         17 my $fpath = "$path/$_->{name}";
168 4 50       181 unlink $fpath or warn "Can't unlink $fpath: $!";
169             }
170 1         11 last;
171             }
172 11         22 $i++;
173             }
174             }
175              
176             # max size
177 15 100       85 if (defined($ms)) {
178 5         12 my $i = 0;
179 5         10 my $tot_size = 0;
180 5         14 for (@entries) {
181 15         29 $tot_size += $_->{size};
182 15 100       41 if ($tot_size > $ms) {
183 1         6 for (splice @entries, $i) {
184 1         7 my $fpath = "$path/$_->{name}";
185 1 50       99 unlink $fpath or warn "Can't unlink $fpath: $!";
186             }
187 1         13 last;
188             }
189 14         50 $i++;
190             }
191             }
192             }
193              
194             1;
195             # ABSTRACT: Write files to a directory, with rotate options
196              
197             __END__