File Coverage

blib/lib/File/Hotfolder.pm
Criterion Covered Total %
statement 27 80 33.7
branch 0 46 0.0
condition 0 26 0.0
subroutine 9 22 40.9
pod 4 7 57.1
total 40 181 22.1


line stmt bran cond sub pod time code
1             package File::Hotfolder;
2 1     1   30045 use strict;
  1         1  
  1         26  
3 1     1   4 use warnings;
  1         1  
  1         27  
4 1     1   6 use v5.10;
  1         5  
  1         39  
5              
6             our $VERSION = '0.03';
7              
8 1     1   3 use Carp;
  1         1  
  1         51  
9 1     1   8 use File::Find;
  1         0  
  1         37  
10 1     1   3 use File::Spec;
  1         1  
  1         18  
11 1     1   390 use Linux::Inotify2;
  1         2305  
  1         150  
12              
13 1     1   10 use parent 'Exporter';
  1         2  
  1         9  
14             our %EXPORT_TAGS = (print => [qw(WATCH_DIR FOUND_FILE DELETE_FILE CATCH_ERROR)]);
15             our @EXPORT = ('watch', @{$EXPORT_TAGS{'print'}});
16             $EXPORT_TAGS{all} = \@EXPORT;
17              
18             use constant {
19 1         726 WATCH_DIR => 1,
20             FOUND_FILE => 2,
21             DELETE_FILE => 4,
22             CATCH_ERROR => 8,
23 1     1   123 };
  1         5  
24              
25             # function interface
26             sub watch {
27 0 0   0 1   shift if $_[0] eq 'File::Hotfolder';
28 0 0         File::Hotfolder->new( @_ % 2 ? (watch => @_) : @_ );
29             }
30              
31             # object interface
32             sub new {
33 0     0 0   my ($class, %args) = @_;
34              
35 0   0       my $path = $args{watch} // '';
36 0 0         $path = File::Spec->rel2abs($path) if $args{fullname};
37             croak "Missing watch directory: $path" unless -d $path,
38              
39             my $self = bless {
40             inotify => (Linux::Inotify2->new
41             or croak "Unable to create new inotify object: $!"),
42 0 0 0 0     callback => ($args{callback} || sub { 1 }),
  0   0        
      0        
43             delete => !!$args{delete},
44             print => 0+($args{print} || 0),
45             filter => $args{filter},
46             scan => $args{scan},
47             catch => $args{catch},
48             }, $class;
49              
50 0 0 0 0     $self->{catch} //= sub { } if ($self->{print} & CATCH_ERROR);
  0            
51              
52 0           $self->watch_recursive( $path );
53              
54 0           $self;
55             }
56              
57             sub watch_recursive {
58 0     0 0   my ($self, $path) = @_;
59              
60             find({
61             no_chdir => 1,
62             wanted => sub {
63 0 0   0     if (-d $_) {
    0          
64 0           $self->watch_directory($_);
65             } elsif( $self->{scan} ) {
66             # TODO: check if not open or modified (lsof or fuser)
67 0           $self->_callback($_);
68             }
69             },
70 0           }, $path );
71             }
72              
73             sub watch_directory {
74 0     0 0   my ($self, $path) = @_;
75              
76 0 0         unless (-d $path) {
77 0           warn "missing watch directory: $path\n";
78 0           return;
79             }
80            
81 0 0         say "watching $path" if ($self->{print} & WATCH_DIR);
82              
83 0 0         unless ( $self->inotify->watch(
84             $path,
85             IN_CREATE | IN_CLOSE_WRITE | IN_MOVE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF,
86             sub {
87 0     0     my $e = shift;
88 0           my $path = $e->fullname;
89            
90 0 0         warn "event queue overflowed\n" if $e->IN_Q_OVERFLOW;
91            
92 0 0 0       if ( $e->IN_ISDIR ) {
    0          
93 0 0 0       if ( $e->IN_CREATE || $e->IN_MOVED_TO) {
    0 0        
94 0           $self->watch_recursive($path);
95             } elsif ( $e->IN_DELETE_SELF || $e->IN_MOVE_SELF ) {
96 0 0         say "unwatching $path" if ($self->{print} & WATCH_DIR);
97 0           $e->w->cancel;
98             }
99             } elsif ( $e->IN_CLOSE_WRITE || $e->IN_MOVED_TO ) {
100 0           $self->_callback($path);
101             }
102              
103             }
104             ) ) {
105 0           warn "watching $path failed: $!\n";
106             };
107             }
108              
109             sub _callback {
110 0     0     my ($self, $path) = @_;
111              
112 0 0 0       if ($self->{filter} && $path !~ $self->{filter}) {
113 0           return;
114             }
115              
116 0 0         say $path if ($self->{print} & FOUND_FILE);
117            
118 0           my $delete;
119 0 0         if ($self->{catch}) {
120 0           $delete = eval { $self->{callback}->($path) };
  0            
121 0 0         if ($@) {
122 0 0         print "$path: $@" if $self->{print} & CATCH_ERROR;
123 0           $self->{catch}->($path, $@);
124 0           return;
125             }
126             } else {
127 0           $delete = $self->{callback}->($path);
128             }
129              
130 0 0 0       if ( $delete && $self->{delete} ) {
131 0 0         say $path if ($self->{print} & DELETE_FILE);
132 0           unlink $path;
133             }
134             }
135              
136             sub inotify {
137 0     0 1   $_[0]->{inotify};
138             }
139              
140             sub loop {
141 0     0 1   1 while $_[0]->inotify->poll;
142             }
143              
144             sub anyevent {
145 0     0 1   my $inotify = $_[0]->inotify;
146             AnyEvent->io (
147 0     0     fh => $inotify->fileno, poll => 'r', cb => sub { $inotify->poll }
148 0           );
149             }
150              
151             1;
152             __END__