File Coverage

blib/lib/AnyEvent/Inotify/Simple.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package AnyEvent::Inotify::Simple;
2             $AnyEvent::Inotify::Simple::VERSION = '0.03';
3 2     2   67721 use Moose;
  0            
  0            
4              
5             # ABSTRACT: monitor a directory tree in a non-blocking way
6              
7             use MooseX::FileAttribute;
8             use MooseX::Types::Moose qw(HashRef CodeRef);
9             use MooseX::Types -declare => ['Receiver'];
10              
11             use AnyEvent::Inotify::EventReceiver;
12             use AnyEvent::Inotify::EventReceiver::Callback;
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             use AnyEvent;
23             use Linux::Inotify2;
24             use File::Next;
25              
26             use namespace::clean -except => ['meta'];
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             my $self = shift;
58              
59             Linux::Inotify2->new or confess "Inotify initialization failed: $!";
60             }
61              
62             has 'io_watcher' => (
63             init_arg => undef,
64             is => 'ro',
65             builder => '_build_io_watcher',
66             required => 1,
67             );
68              
69             sub _build_io_watcher {
70             my $self = shift;
71              
72             return AnyEvent->io(
73             fh => $self->fileno,
74             poll => 'r',
75             cb => sub { $self->poll },
76             );
77             }
78              
79             has 'cookie_jar' => (
80             init_arg => undef,
81             is => 'ro',
82             isa => HashRef,
83             required => 1,
84             default => sub { +{} },
85             );
86              
87             sub _watch_directory {
88             my ($self, $dir) = @_;
89              
90             my $next = File::Next::dirs({
91             follow_symlinks => 0,
92             }, $dir);
93              
94             while ( my $entry = $next->() ) {
95             last unless defined $entry;
96             next if $self->is_filtered($entry);
97              
98             if( -d $entry ){
99             $entry = Path::Class::dir($entry);
100             }
101             else {
102             $entry = Path::Class::file($entry);
103             }
104              
105             $self->watch(
106             $entry->stringify,
107             IN_ALL_EVENTS,
108             sub { $self->handle_event($entry, $_[0]) },
109             );
110             }
111             }
112              
113             sub BUILD {
114             my $self = shift;
115              
116             $self->_watch_directory($self->directory->resolve->absolute);
117             }
118              
119             my %events = (
120             IN_ACCESS => 'handle_access',
121             IN_MODIFY => 'handle_modify',
122             IN_ATTRIB => 'handle_attribute_change',
123             IN_CLOSE_WRITE => 'handle_close_write',
124             IN_CLOSE_NOWRITE => 'handle_close_nowrite',
125             IN_OPEN => 'handle_open',
126             IN_CREATE => 'handle_create',
127             IN_DELETE => 'handle_delete',
128             );
129              
130             sub handle_event {
131             my ($self, $file, $event) = @_;
132              
133             my $wrapper = $event->IN_ISDIR ? 'subdir' : 'file';
134             my $event_file = $file->$wrapper($event->name);
135              
136             if( $event->IN_DELETE_SELF || $event->IN_MOVE_SELF ){
137             #warn "canceling $file";
138             #$event->w->cancel;
139             return;
140             }
141              
142             if($self->is_filtered($event_file)){
143             # we get this when a directory watcher notices something
144             # about a file that should be ignored
145             return;
146             }
147              
148             my $relative = $event_file->relative($self->directory);
149             my $handled = 0;
150              
151             for my $type (keys %events){
152             my $method = $events{$type};
153             if( $event->$type ){
154             $self->$method($relative);
155             $handled = 1;
156             }
157             }
158              
159             if( $event->IN_MOVED_FROM ){
160             $self->handle_move_from($relative, $event->cookie);
161             $handled = 1;
162             }
163              
164             if( $event->IN_MOVED_TO ){
165             $self->handle_move_to($relative, $event->cookie);
166             $handled = 1;
167             }
168              
169             if (!$handled){
170             require Data::Dump::Streamer;
171             Carp::cluck "BUGBUG: Unhandled event: ".
172             Data::Dump::Streamer->Dump($event)->Out;
173             }
174              
175             }
176              
177             sub rel2abs {
178             my ($self, $file) = @_;
179              
180             return $file if $file->is_absolute;
181             return $file->absolute($self->directory)->resolve->absolute;
182             }
183              
184             sub handle_move_from {
185             my ($self, $file, $cookie) = @_;
186              
187             $self->cookie_jar->{from}{$cookie} = $file;
188             }
189              
190             sub handle_move_to {
191             my ($self, $to, $cookie) = @_;
192              
193             my $from = delete $self->cookie_jar->{from}{$cookie};
194             confess "Invalid move cookie '$cookie' (moved to '$to')"
195             unless $from;
196              
197             my $abs = eval { $self->rel2abs($to) };
198             $self->_watch_directory($abs) if $abs && -d $abs;
199              
200             $self->handle_move($from, $to);
201             }
202              
203             # inject our magic
204             before 'handle_create' => sub {
205             my ($self, $dir) = @_;
206             my $abs = eval { $self->rel2abs($dir) };
207             return unless $abs && -d $abs;
208             $self->_watch_directory($abs);
209             };
210              
211             sub DEMOLISH {
212             my $self = shift;
213             return unless $self->inotify;
214             for my $w (values %{$self->inotify->{w}}){
215             next unless $w;
216             $w->cancel;
217             }
218             }
219              
220             1;
221              
222             __END__
223              
224             =head1 NAME
225              
226             AnyEvent::Inotify::Simple - monitor a directory tree in a non-blocking way
227              
228             =head1 VERSION
229              
230             version 0.03
231              
232             =head1 SYNOPSIS
233              
234             use AnyEvent::Inotify::Simple;
235             use EV; # or POE, or Event, or ...
236              
237             my $inotify = AnyEvent::Inotify::Simple->new(
238             directory => '/tmp/uploads/',
239             event_receiver => sub {
240             my ($event, $file, $moved_to) = @_;
241             given($event) {
242             when('create'){
243             say "Someone just uploaded $file!"
244             }
245             };
246             },
247             );
248              
249             EV::loop;
250              
251             =head1 DESCRIPTION
252              
253             This module is a wrapper around L<Linux::Inotify2> that integrates it
254             with an L<AnyEvent> event loop and makes monitoring a directory
255             simple. Provide it with a C<directory>, C<event_receiver>
256             (L<AnyEvent::Inotify::Simple::EventReceiver>), and an optional coderef
257             C<filter>, and it will monitor an entire directory tree. If something
258             is added, it will start watching it. If something goes away, it will
259             stop watching it. It also converts C<IN_MOVE_FROM> and C<IN_MOVE_TO>
260             into one virtual event.
261              
262             Someday I will write more, but that's really all that happens!
263              
264             =head1 METHODS
265              
266             None! Create the object, and it starts working immediately. Destroy
267             the object, and the inotify state and watchers are automatically
268             cleaned up.
269              
270             =head1 REPOSITORY
271              
272             Forks welcome!
273              
274             L<http://github.com/jrockway/anyevent-inotify-simple>
275              
276             =head1 AUTHOR
277              
278             Jonathan Rockway C<< <jrockway@cpan.org> >>
279              
280             Current maintainer is Robert Norris C<< <rob@eatenbyagrue.org> >>
281              
282             =head1 COPYRIGHT
283              
284             Copyright 2009 (c) Jonathan Rockway. This module is Free Software.
285             You may redistribute it under the same terms as Perl itself.