File Coverage

blib/lib/Dir/Write/Rotate.pm
Criterion Covered Total %
statement 120 130 92.3
branch 52 68 76.4
condition 12 15 80.0
subroutine 9 9 100.0
pod 3 3 100.0
total 196 225 87.1


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