File Coverage

blib/lib/File/Hotfolder.pm
Criterion Covered Total %
statement 29 111 26.1
branch 0 62 0.0
condition 0 39 0.0
subroutine 10 32 31.2
pod 4 7 57.1
total 43 251 17.1


line stmt bran cond sub pod time code
1             package File::Hotfolder;
2 2     2   188920 use warnings;
  2         15  
  2         63  
3 2     2   21 use v5.10;
  2         6  
4              
5             our $VERSION = '0.06';
6              
7 2     2   11 use Carp;
  2         3  
  2         97  
8 2     2   21 use File::Find;
  2         3  
  2         189  
9 2     2   13 use File::Spec;
  2         4  
  2         49  
10 2     2   10 use File::Basename qw(basename);
  2         3  
  2         179  
11 2     2   990 use Linux::Inotify2;
  2         5090  
  2         215  
12 2     2   16 use Scalar::Util qw(blessed);
  2         3  
  2         107  
13              
14 2     2   13 use parent 'Exporter';
  2         4  
  2         10  
15             our %EXPORT_TAGS = (print => [qw(
16             WATCH_DIR FOUND_FILE KEEP_FILE DELETE_FILE
17             CATCH_ERROR WATCH_ERROR HOTFOLDER_ERROR
18             HOTFOLDER_ALL
19             )]);
20             our @EXPORT = ('watch', @{$EXPORT_TAGS{'print'}});
21             $EXPORT_TAGS{all} = \@EXPORT;
22              
23             use constant {
24 2         3386 WATCH_DIR => 1,
25             UNWATCH_DIR => 2,
26             FOUND_FILE => 4,
27             KEEP_FILE => 8,
28             DELETE_FILE => 16,
29             CATCH_ERROR => 32,
30             WATCH_ERROR => 64,
31             HOTFOLDER_ALL => 128-1,
32             HOTFOLDER_ERROR => 32 | 64,
33 2     2   315 };
  2         14  
34              
35             # function interface
36             sub watch {
37 0 0   0 1   shift if $_[0] eq 'File::Hotfolder';
38 0 0         File::Hotfolder->new( @_ % 2 ? (watch => @_) : @_ );
39             }
40              
41             # object interface
42             sub new {
43 0     0 0   my ($class, %args) = @_;
44              
45 0   0       my $path = $args{watch} // '';
46 0 0         $path = File::Spec->rel2abs($path) if $args{fullname};
47             croak "Missing watch directory: $path" unless -d $path,
48              
49             my $self = bless {
50             inotify => (Linux::Inotify2->new
51             or croak "Unable to create new inotify object: $!"),
52 0     0     callback => ($args{callback} || sub { 1 }),
53             delete => !!$args{delete},
54             fork => !!$args{fork},
55             print => 0+($args{print} || 0),
56             filter => _build_filter($args{filter},
57 0     0     sub { $_[0] !~ qr{^(.*/)?\.[^/]*$} }),
58             filter_dir => _build_filter($args{filter_dir}, qr{^[^.]|^.$}),
59             scan => $args{scan},
60             catch => _build_catch($args{catch}),
61             logger => _build_logger($args{logger}),
62 0 0 0       event_mask => ($args{event_mask} || ( IN_CLOSE_WRITE | IN_MOVED_TO )),
      0        
      0        
      0        
63             }, $class;
64              
65 0           $self->watch_recursive( $path, 1 );
66              
67 0           $self;
68             }
69              
70             sub _build_catch {
71 0     0     my ($catch) = @_;
72 0 0 0       return $catch if ref $catch // '' eq 'CODE';
73 0 0   0     return $catch ? sub { } : undef;
74             }
75              
76             sub _build_filter {
77 0   0 0     my $filter = $_[0] // $_[1];
78 0 0         return unless $filter;
79 0 0   0     return sub { $_[0] =~ $filter } if ref $filter eq ref qr//;
  0            
80 0           $filter;
81             }
82              
83             sub watch_recursive {
84 0     0 0   my ($self, $path, $is_root) = @_;
85              
86             my $args = {
87             no_chdir => 1,
88             wanted => sub {
89 0 0 0 0     if (-d $_) {
    0          
90 0           $self->_watch_directory($_);
91             } elsif( !$is_root || $self->{scan} ) {
92             # TODO: check if not open or modified (lsof or fuser)
93 0           $self->_callback($_);
94             }
95             },
96 0           };
97              
98 0 0         if ($self->{filter_dir}) {
99 0 0         return unless $self->{filter_dir}->(basename($path));
100             $args->{preprocess} = sub {
101 0     0     grep { $self->{filter_dir}->($_) } @_
  0            
102 0           };
103             }
104            
105 0           find( $args, $path );
106             }
107              
108             sub _watch_directory {
109 0     0     my ($self, $path) = @_;
110              
111 0           $self->log( WATCH_DIR, $path );
112              
113 0 0         unless ( $self->inotify->watch(
114             $path,
115             IN_CREATE | IN_CLOSE_WRITE | IN_MOVE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF,
116             sub {
117 0     0     my $e = shift;
118 0           my $path = $e->fullname;
119            
120 0 0         if ( $e->IN_Q_OVERFLOW ) {
121 0           $self->log( WATCH_ERROR, $path, "event queue overflowed" );
122             }
123            
124 0 0         if ( $e->IN_ISDIR ) {
    0          
125 0 0 0       if ( $e->IN_CREATE || $e->IN_MOVED_TO) {
    0 0        
126 0           $self->watch_recursive($path);
127             } elsif ( $e->IN_DELETE_SELF || $e->IN_MOVE_SELF ) {
128 0           $self->log( UNWATCH_DIR, $path );
129 0           $e->w->cancel;
130             }
131             } elsif ( $e->mask & $self->{event_mask} ) {
132 0           $self->_callback($path);
133             }
134              
135             }
136             ) ) {
137 0           $self->log( WATCH_ERROR, $path, "failed to create watch: $!" );
138             };
139             }
140              
141             sub _callback {
142 0     0     my ($self, $path) = @_;
143              
144 0 0 0       if ($self->{filter} && !$self->{filter}->($path)) {
145 0           return;
146             }
147              
148 0 0         my $fork = $self->{fork} ? fork : undef;
149 0 0         return if $fork; # parent
150              
151 0           $self->log( FOUND_FILE, $path );
152            
153 0           my $status;
154 0 0         if ($self->{catch}) {
155 0           $status = eval { $self->{callback}->($path) };
  0            
156 0 0         if ($@) {
157 0           $self->log( CATCH_ERROR, $path, $@ );
158 0           $self->{catch}->($path, $@);
159 0           return;
160             }
161             } else {
162 0           $status = $self->{callback}->($path);
163             }
164              
165 0 0 0       if ( $status && $self->{delete} ) {
166 0           unlink $path;
167 0           $self->log( DELETE_FILE, $path );
168             } else {
169 0           $self->log( KEEP_FILE, $path );
170             }
171              
172 0 0 0       exit if (defined $fork and !$fork); # child
173             }
174              
175             sub loop {
176 0     0 1   1 while $_[0]->inotify->poll;
177             }
178              
179             sub anyevent {
180 0     0 1   my $inotify = $_[0]->inotify;
181             AnyEvent->io (
182 0     0     fh => $inotify->fileno, poll => 'r', cb => sub { $inotify->poll }
183 0           );
184             }
185              
186             sub inotify {
187 0     0 1   $_[0]->{inotify};
188             }
189              
190             ## LOGGING
191              
192             our %LOGS = (
193             WATCH_DIR , "watching %s",
194             UNWATCH_DIR , "unwatching %s",
195             FOUND_FILE , "found %s",
196             KEEP_FILE , "keep %s",
197             DELETE_FILE , "delete %s",
198             CATCH_ERROR , "error %s",
199             WATCH_ERROR , "failed %s",
200             );
201              
202             sub _build_logger {
203 0     0     my ($logger) = @_;
204              
205 0 0 0       if ( not defined $logger ) {
    0 0        
    0          
206             sub {
207 0     0     my (%args) = @_;
208 0 0         my $fh = $args{event} & HOTFOLDER_ERROR ? *STDERR : *STDOUT;
209 0           say $fh $args{message};
210             }
211 0           } elsif (blessed $logger && $logger->can('log')) {
212             sub {
213 0     0     my (%args) = @_;
214             $logger->log(
215             level => $args{event} & HOTFOLDER_ERROR ? 'error' : 'info',
216             message => $args{message}
217 0 0         );
218             }
219 0           } elsif (ref $logger // '' eq 'CODE') {
220 0           $logger;
221             } else {
222 0           croak "logger must be code or provide a log method!";
223             }
224             }
225              
226             sub log {
227 0     0 0   my ($self, $event, $path, $error) = @_;
228 0 0         if ( $event & $self->{print} ) {
229             $self->{logger}->(
230             event => $event,
231             path => $path,
232 0           message => sprintf($LOGS{$event}, $path),
233             );
234             }
235             }
236              
237             1;
238             __END__