File Coverage

lib/CSS/Watcher/Monitor.pm
Criterion Covered Total %
statement 79 80 98.7
branch 13 18 72.2
condition 18 24 75.0
subroutine 18 19 94.7
pod 0 5 0.0
total 128 146 87.6


line stmt bran cond sub pod time code
1             package CSS::Watcher::Monitor;
2              
3 2     2   43993 use strict;
  2         4  
  2         54  
4 2     2   10 use warnings;
  2         3  
  2         54  
5              
6 2     2   10 use Carp;
  2         4  
  2         114  
7 2     2   1308 use Log::Log4perl qw(:easy);
  2         54712  
  2         11  
8 2     2   1259 use File::Spec;
  2         4  
  2         62  
9 2     2   9 use Fcntl ':mode';
  2         4  
  2         670  
10 2     2   907 use List::MoreUtils qw(any);
  2         12218  
  2         17  
11              
12             our @STAT_FIELDS = qw(
13             dev inode mode num_links uid gid rdev size atime mtime ctime
14             blk_size blocks
15             );
16              
17             sub new {
18 8     8 0 12754 my $class= shift;
19 8         19 my $options = shift;
20              
21             return bless ({
22             dir => $options->{dir} // undef,
23 8   100     85 oldstats => {},
24             }, $class);
25             }
26              
27             sub dir {
28 33     33 0 50 my $self = shift;
29 33 50       79 croak "dir attribute is read-only" if @_;
30 33         283 return $self->{dir};
31             }
32              
33             sub scan {
34 14     14 0 5358 my ($self, $callback, $skip_dirs) = @_;
35              
36 14 100 100     79 return 0 unless (defined $callback && defined $self->dir && -d $self->dir);
      100        
37              
38 10         90 my $newstat = $self->_get_files_info( $self->dir, $skip_dirs );
39              
40 10         17 my $changes = 0;
41 10         14 while ( my( $fname, $stat ) = each %{$newstat->{files}} ) {
  54         1225  
42 44 100       108 unless ($self->_deep_compare ($self->_get_stat ($fname), $stat )) {
43 29         1853 $self->_set_stat ($fname, $stat);
44 29         70 $callback->($fname);
45 29         4492 $changes++;
46             }
47             }
48 10         58 return $changes;
49             }
50              
51             sub is_changed {
52 6     6 0 2001772 my ( $self, $filename ) = @_;
53 6         13 my %objstat;
54 6         90 @objstat{@STAT_FIELDS} = stat ( $filename );
55              
56             # this file may never present before and not exist, return false
57 6 100 66     173 return 0 unless (defined ($objstat{atime}) && -f $filename);
58              
59 5         68 not $self->_deep_compare (
60             $self->_get_stat ($filename),
61             \%objstat);
62             }
63              
64             sub make_dirty {
65 4     4 0 1677 my $self = shift;
66 4         12 $self->{oldstats} = {};
67             }
68              
69             sub _get_stat {
70 49     49   75 my ( $self, $filename ) = @_;
71 49   100     256 return $self->{oldstats}{$filename} // {};
72             }
73              
74             sub _set_stat {
75 29     29   48 my ( $self, $filename, $stat ) = @_;
76 29         68 $self->{oldstats}{$filename} = $stat;
77             }
78              
79             sub _deep_compare {
80 49     49   102 my ( $self, $this, $that ) = @_;
81 2     2   4098 use Storable qw/freeze/;
  2         8699  
  2         1539  
82 49         84 local $Storable::canonical = 1;
83 49         138 return freeze( $this ) eq freeze( $that );
84             }
85              
86             # Scan our target object
87             sub _get_files_info {
88 10     10   21 my ( $self, $dir, $skip_dirs ) = @_;
89 10         15 my %info;
90              
91 10   100     39 $skip_dirs ||= [];
92            
93 10         18 eval {
94 10 50       85 if ( -d $dir ) {
95              
96             # Expand whole directory tree
97 10         80 my @work = $self->_read_dir( $dir );
98 10         40 while ( my $obj = shift @work ) {
99             next # // skip symlinks that have "../" (circular symlink)
100 54 50 66     870 if ( -d $obj
      33        
101             && -l $obj
102             && readlink($obj) =~ m|\.\./| );
103 54 100 33     775 if (-f $obj) {
    50          
104 44         47 my %objstat;
105 44         756 @objstat{@STAT_FIELDS} = stat ( $obj );
106 44         228 $info{ files }{ $obj } = \%objstat;
107             }
108 0     0   0 elsif ( -d $obj && ( !any { $obj =~ m/$_/; } @{$skip_dirs} ) ) {
  10         89  
109             # Depth first to simulate recursion
110 10         27 unshift @work, $self->_read_dir( $obj );
111             }
112             }
113             }
114             };
115              
116 10         24 $info{error} = $@;
117              
118 10         24 return \%info;
119             }
120              
121             sub _read_dir {
122 20     20   31 my $self = shift;
123 20         64 my $dir = shift;
124              
125 20 50       377 opendir( my $dh, $dir ) or LOGDIE "Can't read $dir ($!)";
126 54         552 my @files = map { File::Spec->catfile( $dir, $_ ) }
127             sort
128 20         406 grep { $_ !~ /^[.]{1,2}$/ } readdir( $dh );
  94         307  
129 20         199 closedir( $dh );
130              
131 20         146 return @files;
132             }
133              
134              
135             1;
136              
137             =head1 NAME
138              
139             CSS::Watcher::Monitor - Monitor files for changes.
140              
141             =head1 SYNOPSIS
142              
143             use CSS::Watcher::Monitor;
144             my $cm = CSS::Watcher::Monitor->new (dir => '/foo/bar');
145              
146             # return num of files modified
147             $cm->scan(
148             sub {
149             my $file = shift;
150             # process changed file or first scan new file
151             } );
152              
153             # Check does file changed since last $cm->scan
154             say $cm->is_changed('/foo/bar/baz.txt');
155              
156             # clean old file stat cache
157             $cm->make_dirty();
158              
159             =head1 DESCRIPTION
160              
161             Watch for changes, call callback sub. Call callback on first scan too.
162              
163             =head1 SEE ALSO
164              
165             File::Monitor - I get some patterns from there
166              
167             =head1 AUTHOR
168              
169             Olexandr Sydorchuk (olexandr.syd@gmail.com)
170              
171             =head1 COPYRIGHT AND LICENSE
172              
173             Copyright (C) 2014 by Olexandr Sydorchuk
174              
175             This program is free software; you can redistribute it and/or modify
176             it under the same terms as Perl itself, either Perl version 5.8.2 or,
177             at your option, any later version of Perl 5 you may have available.
178              
179             =cut