File Coverage

blib/lib/AnyEvent/Inotify/Simple.pm
Criterion Covered Total %
statement 86 97 88.6
branch 21 32 65.6
condition 4 8 50.0
subroutine 21 23 91.3
pod 0 6 0.0
total 132 166 79.5


line stmt bran cond sub pod time code
1             package AnyEvent::Inotify::Simple;
2             $AnyEvent::Inotify::Simple::VERSION = '0.04';
3 3     3   3353681 use Moose;
  3         963846  
  3         20  
4              
5             # ABSTRACT: monitor a directory tree in a non-blocking way
6              
7 3     3   24378 use MooseX::FileAttribute;
  3         447557  
  3         18  
8 3     3   2415 use MooseX::Types::Moose qw(HashRef CodeRef ArrayRef Int);
  3         8  
  3         25  
9 3     3   16602 use MooseX::Types -declare => ['Receiver'];
  3         9  
  3         27  
10              
11 3     3   17196 use AnyEvent::Inotify::EventReceiver;
  3         9  
  3         122  
12 3     3   1383 use AnyEvent::Inotify::EventReceiver::Callback;
  3         12  
  3         418  
13              
14             role_type Receiver, { role => 'AnyEvent::Inotify::EventReceiver' };
15              
16             coerce Receiver, from CodeRef, via {
17             AnyEvent::Inotify::EventReceiver::Callback->new(
18             callback => $_,
19             ),
20             };
21              
22 3     3   2385 use AnyEvent;
  3         11728  
  3         95  
23 3     3   1728 use Linux::Inotify2;
  3         9151  
  3         408  
24 3     3   1542 use File::Next;
  3         6593  
  3         133  
25              
26 3     3   26 use namespace::clean -except => ['meta'];
  3         8  
  3         41  
27              
28             has_directory 'directory' => ( must_exist => 1, required => 1);
29              
30             has 'filter' => (
31             traits => ['Code'],
32             is => 'ro',
33             isa => CodeRef,
34             handles => { is_filtered => 'execute' },
35             default => sub {
36             sub { return 0 },
37             },
38             );
39              
40             has 'event_receiver' => (
41             is => 'ro',
42             isa => Receiver,
43             handles => 'AnyEvent::Inotify::EventReceiver',
44             required => 1,
45             coerce => 1,
46             );
47              
48             has 'inotify' => (
49             init_arg => undef,
50             is => 'ro',
51             isa => 'Linux::Inotify2',
52             handles => [qw/poll fileno watch/],
53             lazy_build => 1,
54             );
55              
56             sub _build_inotify {
57 5     5   15 my $self = shift;
58              
59 5 50       61 my $inotify = Linux::Inotify2->new or confess "Inotify initialization failed: $!";
60             # Ignore overflows, rather than broadcasting to every watcher
61 5     0   676 $inotify->on_overflow(sub {});
62 5         224 return $inotify;
63             }
64              
65             has 'wanted_events' => (
66             is => 'ro',
67             isa => ArrayRef,
68             default => sub {
69             [ qw(access modify attribute_change close_write close_nowrite open create delete move) ]
70             },
71             );
72              
73             has '_wanted_events_mask' => (
74             is => 'ro',
75             isa => Int,
76             lazy_build => 1,
77             );
78              
79             my %event_to_mask = (
80             access => IN_ACCESS,
81             modify => IN_MODIFY,
82             attribute_change => IN_ATTRIB,
83             close_write => IN_CLOSE_WRITE,
84             close_nowrite => IN_CLOSE_NOWRITE,
85             open => IN_OPEN,
86             create => IN_CREATE,
87             delete => IN_DELETE,
88             move => IN_MOVED_FROM | IN_MOVED_TO,
89             );
90              
91             sub _build__wanted_events_mask {
92 5     5   14 my $mask = 0;
93 5         12 for (@{$_[0]->wanted_events}) {
  5         182  
94 23   50     80 my $event_mask = $event_to_mask{$_}
95             || die "Unknown wanted event: $_";
96 23         40 $mask |= $event_mask;
97             }
98 5         157 return $mask;
99             }
100              
101             has 'io_watcher' => (
102             init_arg => undef,
103             is => 'ro',
104             builder => '_build_io_watcher',
105             required => 1,
106             );
107              
108             sub _build_io_watcher {
109 5     5   6163 my $self = shift;
110              
111             return AnyEvent->io(
112             fh => $self->fileno,
113             poll => 'r',
114 1     1   4424 cb => sub { $self->poll },
115 5         48 );
116             }
117              
118             has 'cookie_jar' => (
119             init_arg => undef,
120             is => 'ro',
121             isa => HashRef,
122             required => 1,
123             default => sub { +{} },
124             );
125              
126             sub _watch_directory {
127 10     10   1297 my ($self, $dir) = @_;
128              
129 10         85 my $next = File::Next::dirs({
130             follow_symlinks => 0,
131             }, $dir);
132              
133 10         1361 while ( my $entry = $next->() ) {
134 12 50       2242 last unless defined $entry;
135 12 100       607 next if $self->is_filtered($entry);
136              
137 11 50       285 if( -d $entry ){
138 11         73 $entry = Path::Class::dir($entry);
139             }
140             else {
141 0         0 $entry = Path::Class::file($entry);
142             }
143              
144             $self->watch(
145             $entry->stringify,
146             $self->_wanted_events_mask,
147 33     33   1373 sub { $self->handle_event($entry, $_[0]) },
148 11         700 );
149             }
150             }
151              
152             sub BUILD {
153 5     5 0 10820 my $self = shift;
154              
155 5         43 $self->_watch_directory($self->directory->resolve->absolute);
156             }
157              
158             my %events = (
159             IN_ACCESS => 'handle_access',
160             IN_MODIFY => 'handle_modify',
161             IN_ATTRIB => 'handle_attribute_change',
162             IN_CLOSE_WRITE => 'handle_close_write',
163             IN_CLOSE_NOWRITE => 'handle_close_nowrite',
164             IN_OPEN => 'handle_open',
165             IN_CREATE => 'handle_create',
166             IN_DELETE => 'handle_delete',
167             );
168              
169             sub handle_event {
170 33     33 0 99 my ($self, $file, $event) = @_;
171              
172 33 100       108 my $wrapper = $event->IN_ISDIR ? 'subdir' : 'file';
173 33         266 my $event_file = $file->$wrapper($event->name);
174              
175 33 50 33     2512 if( $event->IN_DELETE_SELF || $event->IN_MOVE_SELF ){
176             #warn "canceling $file";
177             #$event->w->cancel;
178 0         0 return;
179             }
180              
181 33 100       1747 if($self->is_filtered($event_file)){
182             # we get this when a directory watcher notices something
183             # about a file that should be ignored
184 8         300 return;
185             }
186              
187 25         448 my $relative = $event_file->relative($self->directory);
188 25         4149 my $handled = 0;
189              
190 25         131 for my $type (keys %events){
191 200         775 my $method = $events{$type};
192 200 100       630 if( $event->$type ){
193 21         213 $self->$method($relative);
194 21         1074 $handled = 1;
195             }
196             }
197              
198 25 100       178 if( $event->IN_MOVED_FROM ){
199 2         15 $self->handle_move_from($relative, $event->cookie);
200 2         7 $handled = 1;
201             }
202              
203 25 100       161 if( $event->IN_MOVED_TO ){
204 2         12 $self->handle_move_to($relative, $event->cookie);
205 2         236 $handled = 1;
206             }
207              
208 25 50       208 if (!$handled){
209 0         0 require Data::Dumper;
210 0         0 local $Data::Dumper::Maxdepth = 2;
211 0         0 Carp::cluck "BUGBUG: Unhandled event: ".
212             Data::Dumper->Dump($event);
213             }
214              
215             }
216              
217             sub rel2abs {
218 14     14 0 37 my ($self, $file) = @_;
219              
220 14 50       50 return $file if $file->is_absolute;
221 14         404 return $file->absolute($self->directory)->resolve->absolute;
222             }
223              
224             sub handle_move_from {
225 2     2 0 14 my ($self, $file, $cookie) = @_;
226              
227 2         73 $self->cookie_jar->{from}{$cookie} = $file;
228             }
229              
230             sub handle_move_to {
231 2     2 0 13 my ($self, $to, $cookie) = @_;
232              
233 2         99 my $from = delete $self->cookie_jar->{from}{$cookie};
234 2 50       9 confess "Invalid move cookie '$cookie' (moved to '$to')"
235             unless $from;
236              
237 2         17 my $abs = eval { $self->rel2abs($to) };
  2         9  
238 2 100 66     799 $self->_watch_directory($abs) if $abs && -d $abs;
239              
240 2         180 $self->handle_move($from, $to);
241             }
242              
243             # inject our magic
244             before 'handle_create' => sub {
245             my ($self, $dir) = @_;
246             my $abs = eval { $self->rel2abs($dir) };
247             return unless $abs && -d $abs;
248             $self->_watch_directory($abs);
249             };
250              
251             sub DEMOLISH {
252 0     0 0   my $self = shift;
253 0 0         return unless $self->inotify;
254 0           for my $w (values %{$self->inotify->{w}}){
  0            
255 0 0         next unless $w;
256 0           $w->cancel;
257             }
258             }
259              
260             1;
261              
262             __END__
263              
264             =head1 NAME
265              
266             AnyEvent::Inotify::Simple - monitor a directory tree in a non-blocking way
267              
268             =head1 SYNOPSIS
269              
270             use AnyEvent::Inotify::Simple;
271             use EV; # or POE, or Event, or ...
272              
273             my $inotify = AnyEvent::Inotify::Simple->new(
274             directory => '/tmp/uploads/',
275             wanted_events => [ qw(create move) ],
276             event_receiver => sub {
277             my ($event, $file, $moved_to) = @_;
278             given($event) {
279             when('create'){
280             say "Someone just uploaded $file!"
281             }
282             };
283             },
284             );
285              
286             EV::loop;
287              
288             =head1 DESCRIPTION
289              
290             This module is a wrapper around L<Linux::Inotify2> that integrates it
291             with an L<AnyEvent> event loop and makes monitoring a directory
292             simple. Provide it with a C<directory>, C<event_receiver>
293             (L<AnyEvent::Inotify::Simple::EventReceiver>), and an optional coderef
294             C<filter> and/or optional array ref C<wanted_events>, and it will
295             monitor an entire directory tree. If something
296             is added, it will start watching it. If something goes away, it will
297             stop watching it. It also converts C<IN_MOVE_FROM> and C<IN_MOVE_TO>
298             into one virtual event.
299              
300             Someday I will write more, but that's really all that happens!
301              
302             =head1 METHODS
303              
304             None! Create the object, and it starts working immediately. Destroy
305             the object, and the inotify state and watchers are automatically
306             cleaned up.
307              
308             =head1 REPOSITORY
309              
310             Forks welcome!
311              
312             L<http://github.com/jrockway/anyevent-inotify-simple>
313              
314             =head1 AUTHOR
315              
316             Jonathan Rockway C<< <jrockway@cpan.org> >>
317              
318             Current maintainer is Rob N ★ C<< <robn@robn.io> >>
319              
320             =head1 COPYRIGHT
321              
322             Copyright 2009 (c) Jonathan Rockway. This module is Free Software.
323             You may redistribute it under the same terms as Perl itself.