File Coverage

blib/lib/File/Hotfolder.pm
Criterion Covered Total %
statement 33 113 29.2
branch 0 56 0.0
condition 0 34 0.0
subroutine 11 33 33.3
pod 4 7 57.1
total 48 243 19.7


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