File Coverage

blib/lib/File/ChangeNotify/Watcher.pm
Criterion Covered Total %
statement 117 121 96.6
branch 43 54 79.6
condition 3 3 100.0
subroutine 24 26 92.3
pod 1 2 50.0
total 188 206 91.2


line stmt bran cond sub pod time code
1             package File::ChangeNotify::Watcher;
2              
3 3     3   28616 use strict;
  3         8  
  3         90  
4 3     3   16 use warnings;
  3         6  
  3         145  
5 3     3   17 use namespace::autoclean;
  3         7  
  3         22  
6              
7             our $VERSION = '0.31';
8              
9 3     3   308 use Fcntl qw( S_IMODE );
  3         55  
  3         203  
10 3     3   856 use File::ChangeNotify::Event;
  3         8  
  3         108  
11 3     3   26 use File::Find qw( find );
  3         6  
  3         262  
12 3     3   20 use File::Spec;
  3         5  
  3         63  
13 3     3   22 use Module::Runtime qw( use_module );
  3         5  
  3         26  
14             use Types::Standard
15 3     3   142 qw( ArrayRef Bool ClassName CodeRef HashRef Num RegexpRef Str );
  3         6  
  3         22  
16 3     3   4627 use Type::Utils -all;
  3         6  
  3         20  
17              
18             # Trying to import this just blows up on Win32, and checking
19             # Time::HiRes::d_hires_stat() _also_ blows up on Win32.
20             BEGIN {
21             ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
22 3     3   8944 eval {
23 3         19 require Time::HiRes;
24 3         18 Time::HiRes->import('stat');
25             };
26             }
27              
28 3     3   364 use Moo::Role;
  3         5  
  3         36  
29              
30             has filter => (
31             is => 'ro',
32             isa => RegexpRef,
33             default => sub {qr/.*/},
34             );
35              
36             #<<<
37             my $dir_t = subtype as Str,
38             where { -d $_ },
39             message { "$_ is not a valid directory" };
40              
41             my $array_of_dirs_t = subtype as ArrayRef[Str],
42             where {
43             map {-d} @{$_};
44             },
45             message {"@{$_} is not a list of valid directories"};
46              
47             coerce $array_of_dirs_t,
48             from $dir_t,
49             via { [$_] };
50             #>>>
51              
52             has directories => (
53             is => 'ro',
54             writer => '_set_directories',
55             isa => $array_of_dirs_t,
56             required => 1,
57             coerce => 1,
58             );
59              
60             has follow_symlinks => (
61             is => 'ro',
62             isa => Bool,
63             default => 0,
64             );
65              
66             has event_class => (
67             is => 'ro',
68             isa => ClassName,
69             default => 'File::ChangeNotify::Event',
70             );
71              
72             has sleep_interval => (
73             is => 'ro',
74             isa => Num,
75             default => 2,
76             );
77              
78             my $files_or_regexps_or_code_t
79             = subtype as ArrayRef [ Str | RegexpRef | CodeRef ];
80              
81             has exclude => (
82             is => 'ro',
83             isa => $files_or_regexps_or_code_t,
84             default => sub { [] },
85             );
86              
87             has modify_includes_file_attributes => (
88             is => 'ro',
89             isa => Bool | $files_or_regexps_or_code_t,
90             default => 0,
91             );
92              
93             has modify_includes_content => (
94             is => 'ro',
95             isa => Bool | $files_or_regexps_or_code_t,
96             default => 0,
97             );
98              
99             has _map => (
100             is => 'ro',
101             writer => '_set_map',
102             isa => HashRef,
103             predicate => '_has_map',
104             );
105              
106             sub BUILD {
107 0     0 0 0 my $self = shift;
108              
109 0         0 use_module( $self->event_class );
110             }
111              
112             ## no critic ( Subroutines::ProhibitUnusedPrivateSubroutines)
113             sub _current_map {
114 54     54   85 my $self = shift;
115              
116 54         82 my %map;
117              
118             find(
119             {
120             wanted => sub {
121              
122             # File::Find seems to use '/' as the path separator on Windows
123             # for some odd reason. It really should be using File::Spec
124             # internally everywhere but it doesn't.
125 144 50   144   596 my $path
126             = $^O eq 'MSWin32'
127             ? File::Spec->canonpath($File::Find::name)
128             : $File::Find::name;
129              
130 144 100       332 if ( $self->_path_is_excluded($path) ) {
131 4         9 $File::Find::prune = 1;
132 4         37 return;
133             }
134              
135 140 100       287 my $entry = $self->_entry_for_map($path) or return;
136 138         6423 $map{$path} = $entry;
137             },
138             follow_fast => ( $self->follow_symlinks ? 1 : 0 ),
139             no_chdir => 1,
140             follow_skip => 2,
141             },
142 54 100       551 @{ $self->directories },
  54         17018  
143             );
144              
145 54         864 return \%map;
146             }
147             ## use critic
148              
149             sub _path_is_excluded {
150 150     150   221 my $self = shift;
151 150         222 my $path = shift;
152              
153 150         519 return $self->_path_matches( $self->exclude, $path );
154             }
155              
156             sub _entry_for_map {
157 140     140   204 my $self = shift;
158 140         186 my $path = shift;
159              
160 140 100       1603 my $is_dir = -d $path ? 1 : 0;
161              
162             # This should be free since the stat call was already done when checking
163             # -d.
164 140         1830 my @stat = stat;
165              
166 140 100 100     1465 return if -l $path && !$is_dir;
167              
168 138 100       397 unless ($is_dir) {
169 56         263 my $filter = $self->filter;
170 56 50       972 return unless ( File::Spec->splitpath($path) )[2] =~ /$filter/;
171             }
172              
173             return {
174 138 100       504 is_dir => $is_dir,
    100          
175             size => ( $is_dir ? 0 : $stat[7] ),
176             $self->_maybe_file_attributes( $path, \@stat ),
177             ( $is_dir ? () : $self->_maybe_content($path) ),
178             };
179             }
180              
181             sub _maybe_file_attributes {
182 138     138   199 my $self = shift;
183 138         181 my $path = shift;
184 138         168 my $stat = shift;
185              
186             # The Default watcher always requires the mtime, regardless of whether or
187             # not we're including stat info in the modify events.
188 138 50       366 unless ( $self->_always_requires_mtime ) {
189             return
190 0 0       0 unless $self->_path_matches(
191             $self->modify_includes_file_attributes,
192             $path,
193             );
194             }
195              
196 138         274 return ( stat => $self->_stat( $path, $stat ) );
197             }
198              
199             sub _stat {
200 138     138   184 my $self = shift;
201 138         166 my $path = shift;
202 138         174 my $stat = shift;
203              
204 138 50       230 my @stat = $stat ? @{$stat} : stat $path;
  138         2247  
205             return {
206 138         1218 attributes => {
207             permissions => S_IMODE( $stat[2] ),
208             uid => $stat[4],
209             gid => $stat[5],
210             },
211             mtime => $stat[9],
212             };
213             }
214              
215 0     0   0 sub _always_requires_mtime {0}
216              
217             sub _maybe_content {
218 56     56   108 my $self = shift;
219 56         150 my $path = shift;
220              
221             return
222 56 100       160 unless $self->_path_matches( $self->modify_includes_content, $path );
223              
224 8 50       244 open my $fh, '<', $path or die "Cannot open $path for reading: $!";
225 8 50       61 binmode $fh, ':bytes' or die qq{Cannot binmode $path as ':bytes': $!};
226 8         12 my $content = do {
227 8         35 local $/ = undef;
228 8         168 <$fh>;
229             };
230 8 50       75 close $fh or die "Cannot close $path: $!";
231              
232 8         72 return ( content => $content );
233             }
234              
235             sub new_events {
236 17     17 1 3328 my $self = shift;
237              
238 17         63 return $self->_interesting_events;
239             }
240              
241             ## no critic ( Subroutines::ProhibitUnusedPrivateSubroutines)
242             sub _modify_event_maybe_file_attribute_changes {
243 47     47   72 my $self = shift;
244 47         76 my $path = shift;
245 47         66 my $old_map = shift;
246 47         60 my $new_map = shift;
247              
248             return
249 47 100       127 unless $self->_path_matches(
250             $self->modify_includes_file_attributes,
251             $path,
252             );
253              
254 8         15 my $old_attr = $old_map->{$path}{stat}{attributes};
255 8         14 my $new_attr = $new_map->{$path}{stat}{attributes};
256              
257 8         13 for my $k ( keys %{$new_attr} ) {
  8         22  
258              
259             # Any possible info retrieved from stat will be numeric, so we can
260             # always use numeric comparison safely.
261             return ( attributes => [ $old_attr, $new_attr ] )
262 19 100       50 if $old_attr->{$k} != $new_attr->{$k};
263             }
264              
265 4         13 return;
266             }
267              
268             sub _modify_event_maybe_content_changes {
269 14     14   27 my $self = shift;
270 14         22 my $path = shift;
271 14         24 my $old_map = shift;
272 14         19 my $new_map = shift;
273              
274             return
275 14 100       36 unless $self->_path_matches( $self->modify_includes_content, $path );
276             return (
277 4         111 content => [ $old_map->{$path}{content}, $new_map->{$path}{content} ]
278             );
279             }
280              
281             sub _path_matches {
282 267     267   363 my $self = shift;
283 267         300 my $matches = shift;
284 267         318 my $path = shift;
285              
286 267 100       995 return $matches if !ref $matches;
287              
288 172         255 foreach my $matcher ( @{$matches} ) {
  172         322  
289 73 100       146 if ( my $ref = ref $matcher ) {
290 54 100       100 if ( $ref eq 'Regexp' ) {
    50          
291 51 100       272 return 1 if $path =~ /$matcher/;
292             }
293             elsif ( $ref eq 'CODE' ) {
294 3         5 local $_ = $path;
295 3 50       7 return 1 if $matcher->($path);
296             }
297             }
298             else {
299 19 100       36 return 1 if $path eq $matcher;
300             }
301             }
302              
303 155         433 return;
304             }
305              
306             sub _remove_directory {
307 2     2   4 my $self = shift;
308 2         5 my $dir = shift;
309              
310             $self->_set_directories(
311 2         5 [ grep { $_ ne $dir } @{ $self->directories } ] );
  2         55  
  2         7  
312             }
313             ## use critic
314              
315             1;
316              
317             # ABSTRACT: Role consumed by all watchers
318              
319             __END__