File Coverage

blib/lib/Net/FTP/Find/Mixin.pm
Criterion Covered Total %
statement 36 244 14.7
branch 0 168 0.0
condition 1 39 2.5
subroutine 11 21 52.3
pod 0 5 0.0
total 48 477 10.0


line stmt bran cond sub pod time code
1             package Net::FTP::Find::Mixin;
2              
3 1     1   5 use strict;
  1         43  
  1         106  
4 1     1   16 use warnings;
  1         2  
  1         130  
5              
6             our $VERSION = '0.041';
7              
8 1     1   15 use Carp;
  1         2  
  1         62  
9 1     1   4 use File::Spec;
  1         1  
  1         18  
10 1     1   5 use File::Basename;
  1         3  
  1         133  
11 1     1   6 use Time::Local qw(timegm);
  1         2  
  1         72  
12 1     1   8 use Net::Cmd;
  1         3  
  1         83  
13 1     1   1038 use File::Listing;
  1         7603  
  1         95  
14              
15             my @month_name_list = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
16              
17             sub import {
18 1     1   2 my $class = shift;
19 1   50     6 my $pkg = shift || 'Net::FTP';
20              
21 1     1   8 no strict 'refs';
  1         2  
  1         2236  
22 1         2 *{$pkg . '::find'} = \&find;
  1         6  
23 1         2 *{$pkg . '::finddepth'} = \&finddepth;
  1         22  
24             }
25              
26             sub finddepth {
27 0     0 0   my $self = shift;
28 0           my ($opts, @directories) = @_;
29              
30 0           my %options = (
31             bydepth => 1,
32             );
33              
34 0 0         if (ref $opts eq 'CODE') {
    0          
35 0           $options{'wanted'} = $opts;
36             }
37             elsif (ref $opts eq 'HASH') {
38 0           while (my ($k, $v) = each(%$opts)) {
39 0           $options{$k} = $v;
40             }
41             }
42              
43 0           &find($self, \%options, @directories);
44             }
45              
46             sub find {
47 0     0 0   my $self = shift;
48 0           my ($opts, @directories) = @_;
49              
50 0           my %options = (
51             use_mlsd => 1,
52             );
53              
54 0 0         if (ref $opts eq 'CODE') {
    0          
55 0           $options{'wanted'} = $opts;
56             }
57             elsif (ref $opts eq 'HASH') {
58 0           while (my ($k, $v) = each(%$opts)) {
59 0           $options{$k} = $v;
60             }
61             }
62              
63 0 0         if (! $options{'wanted'}) {
64 0           croak('no &wanted subroutine given');
65             }
66              
67 0 0         if ( !$options{'fstype'} ) {
68 0           $options{'fstype'} = 'unix';
69 0           my $res = _command( $self, 'SYST' );
70 0 0         if ( $res->[0] == CMD_OK ) {
71 0 0         if ( $res->[1] =~ m/windows/i ) {
72 0           $options{'fstype'} = 'dosftp';
73             }
74             }
75             }
76              
77 0 0         defined(my $cwd = $self->pwd)
78             or return;
79 0 0         $cwd =~ s{/*\z}{/} if $cwd;
80              
81 0           foreach my $d (@directories) {
82 0 0         &recursive( $self, $d =~ m!\A/! ? '' : $cwd, \%options, $d, 0 )
    0          
83             or return;
84             }
85              
86 0           1;
87             }
88              
89             sub recursive {
90 0     0 0   my $self = shift;
91 0           my ($cwd, $opts, $directory, $depth) = @_;
92              
93             our (
94 0           $name, $dir,
95             $is_directory, $is_symlink, $mode,
96             $permissions, $link, $user, $group, $size, $month, $mday, $year_or_time,
97             $type, $ballpark_mtime,
98             $unix_like_system_size, $unix_like_system_name,
99             $mlsd_facts, $mtime,
100             );
101              
102 0 0 0       return 1
103             if (defined($opts->{'max_depth'}) && $depth > $opts->{'max_depth'});
104              
105 0           local $dir;
106 0           my $orig_cwd = undef;
107 0           my $entries;
108 0 0         if ($opts->{'no_chdir'}) {
109 0 0         $entries
110             = _dir_entries( $self, $opts, $directory, undef, undef, undef,
111             $depth == 0 )
112             or return;
113 0 0         return 1 unless @$entries;
114              
115 0 0         if ($depth == 0) {
116 0 0         if (! grep {$_->{data}[0] eq '.'} @$entries) {
  0            
117 0 0         build_start_dir( $self, $opts, $entries, $directory,
118             dirname($directory) )
119             or return;
120             }
121             }
122              
123 0           $dir = $directory;
124             }
125             else {
126 0 0         defined($orig_cwd = $self->pwd)
127             or return;
128 0 0         if ($orig_cwd) {
129 0           $orig_cwd =~ s{^/*}{/};
130             }
131              
132 0 0         $self->cwd($directory)
133             or return;
134 0 0         $entries
135             = _dir_entries( $self, $opts, '.', undef, undef, undef,
136             $depth == 0 )
137             or return;
138              
139 0 0         defined($dir = $self->pwd)
140             or return;
141 0 0         if ($dir) {
    0          
142 0           $dir =~ s{^/*}{/};
143             }
144             elsif (defined($dir)) {
145 0           $dir = $directory;
146             }
147              
148 0 0         if ($depth == 0) {
149 0 0         if (! grep {$_->{data}[0] eq '.'} @$entries) {
  0            
150 0 0         $self->cwd('..')
151             or return;
152 0 0         build_start_dir($self, $opts, $entries, $directory, '.')
153             or return;
154             }
155              
156 0 0         $self->cwd($orig_cwd)
157             or return;
158             }
159              
160 0 0         if ( !@$entries ) {
161 0 0         $self->cwd($orig_cwd)
162             or return;
163 0           return 1;
164             }
165             }
166              
167 0           my @dirs = ();
168 0           foreach my $e (@$entries) {
169             local (
170 0           $permissions, $link, $user, $group, $unix_like_system_size, $month, $mday, $year_or_time, $unix_like_system_name
171             ) = split(/\s+/, $e->{line}, 9);
172             local (
173 0           $_, $type, $size, $ballpark_mtime, $mode
174 0           ) = @{ $e->{data} };
175 0   0       local $mlsd_facts = $e->{mlsd_facts} || undef;
176              
177 0 0         next if $_ eq '..';
178 0 0 0       next if $_ eq '.' && $depth != 0;
179              
180 0 0         if ($depth == 0) {
181 0 0         next if $_ ne '.';
182 0           $_ = $directory;
183             }
184              
185 0 0         local $name = $depth == 0 ? $_ : File::Spec->catfile($dir, $_);
186 0 0 0       $_ = $name if $opts->{'no_chdir'} && $depth != 0;
187 0           my $next = $_;
188              
189 0 0         $name =~ s/$cwd// if $cwd;
190 0 0         $dir =~ s/$cwd// if $cwd;
191              
192 0           local $is_directory = $type eq 'd';
193 0           local $is_symlink = substr($type, 0, 1) eq 'l';
194              
195 0           local $mtime;
196 0 0 0       if ($mlsd_facts) {
    0          
197 0           $mtime = $ballpark_mtime;
198             }
199             elsif ($type eq 'f' && $opts->{fetch_mtime}) {
200 0           $mtime = _mdtm_gmt($self, $_);
201             }
202              
203 0 0 0       if ($is_directory && $opts->{'bydepth'}) {
204 0 0         &recursive($self, $cwd, $opts, $next, $depth+1)
205             or return;
206             }
207              
208 0 0 0       if (
209             (! defined($opts->{'min_depth'}))
210             || ($depth > $opts->{'min_depth'})
211             ) {
212 0 0 0       local $_ = '.' if (! $opts->{'no_chdir'}) && $depth == 0;
213              
214 1     1   7 no strict 'refs';
  1         23  
  1         1814  
215 0           foreach my $k (qw(
216             name dir is_directory is_symlink mode
217             permissions link user group size month mday year_or_time
218             type ballpark_mtime mtime mlsd_facts
219             )) {
220 0           ${'Net::FTP::Find::'.$k} = $$k;
  0            
221             }
222              
223 0           $opts->{'wanted'}($self);
224             }
225              
226 0 0 0       if ($is_directory && ! $opts->{'bydepth'}) {
227 0 0         &recursive($self, $cwd, $opts, $next, $depth+1)
228             or return;
229             }
230             }
231              
232 0 0         if ($orig_cwd) {
233 0 0         $self->cwd($orig_cwd)
234             or return;
235             }
236              
237 0           1;
238             }
239              
240             sub build_start_dir {
241 0     0 0   my ($self, $opts, $entries, $current, $parent) = @_;
242              
243 0           my $detected = 0;
244 0 0         if ($current ne '/') {
245 0 0         my $parent_entries = _dir_entries( $self, $opts, $parent )
246             or return;
247 0           my $basename = basename($current);
248              
249 0           for my $e (@$parent_entries) {
250 0 0         next if $e->{data}[0] ne $basename;
251              
252 0           $detected = 1;
253 0           $e->{line} =~ s/$basename$/./g;
254 0           $e->{data}[0] = '.';
255 0           splice @$entries, 0, scalar(@$entries), $e;
256             }
257             }
258              
259 0 0         if (! $detected) {
260 0           my ($year, $month, $mday, $hour, $min) = (localtime)[5,4,3,2,1];
261 0           my $line;
262 0 0         if ($opts->{'fstype'} eq 'dosftp') {
263 0 0         $line = join(
264             ' ',
265             sprintf( '%02d-%02d-%d',
266             $month + 1, $mday, substr( $year + 1900, 2 ) ),
267             ( $hour < 12
268             ? sprintf( '%02d:%02dAM', $hour, $min )
269             : sprintf( '%02d:%02dPM', $hour - 12, $min )
270             ),
271             '', '.'
272             );
273             }
274             else {
275 0           $line = join(' ',
276             'drwxr-xr-x',
277             scalar(@$entries)+2,
278             '-',
279             '-',
280             0,
281             $month_name_list[$month],
282             sprintf('%02d', $mday),
283             sprintf('%02d:%02d', $hour, $min),
284             '.'
285             );
286             }
287 0           my ($e) = parse_entries([$line], undef, undef, undef, 1);
288 0           splice @$entries, 0, scalar(@$entries), $e;
289             }
290              
291 0           1;
292             }
293              
294             sub _list {
295 0     0     my $self = shift;
296              
297 0 0         if ( $self->isa('Net::FTPSSL') ) {
298 0           my @entries = $self->list(@_);
299 0 0         if ( $self->last_status_code != CMD_OK ) {
300 0           return;
301             }
302             else {
303 0           [@entries];
304             }
305             }
306             else {
307 0           $self->dir(@_);
308             }
309             }
310              
311             sub _command {
312 0     0     my $self = shift;
313              
314 0 0         if ( $self->isa('Net::FTPSSL') ) {
315 0           my $status = $self->command(@_)->response;
316 0           return [$status, $self->last_message];
317             }
318             else {
319 0           my $status = $self->cmd(@_);
320 0           return [$status, $self->message];
321             }
322             }
323              
324             sub _data_command {
325 0     0     my $self = shift;
326              
327 0           my $res = '';
328 0 0         if ( $self->isa('Net::FTPSSL') ) {
329 0 0         unless ( $self->prep_data_channel ) {
330 0           return;
331             }
332              
333 0 0         if ( $self->command(@_)->response != CMD_INFO ) {
334 0           $self->_croak_or_return;
335 0           return;
336             }
337              
338 0           my ( $tmp, $io, $size );
339              
340 0           $size = ${*$self}{buf_size};
  0            
341              
342 0           $io = $self->_get_data_channel;
343 0 0         unless ( defined $io ) {
344 0           return;
345             }
346              
347 0           while ( my $len = sysread $io, $tmp, $size ) {
348 0 0         unless ( defined $len ) {
349 0 0         next if $! == Net::FTPSSL::EINTR();
350 0           $self->_croak_or_return;
351 0           $io->close;
352 0           return;
353             }
354 0           $res .= $tmp;
355             }
356              
357 0           $io->close;
358              
359 0 0         if ( $self->response() != CMD_OK ) {
360 0           $self->_croak_or_return;
361 0           return;
362             }
363             }
364             else {
365 0 0         my $data = $self->_data_cmd(@_)
366             or return;
367 0           my $buf;
368 0           my $size = ${*$self}{'net_ftp_blksize'};
  0            
369 0           while ( $data->read( $buf, $size ) ) {
370 0           $res .= $buf;
371             }
372             $data->close
373 0 0         or return;
374             }
375              
376 0           $res;
377             }
378              
379             sub _mdtm_gmt {
380 0     0     my $self = shift;
381              
382 0 0         if ( $self->isa('Net::FTPSSL') ) {
383 0           $self->_mdtm(@_);
384             }
385             else {
386 0           $self->mdtm(@_);
387             }
388             }
389              
390             sub _dir_entries {
391 0     0     my $self = shift;
392 0           my ($opts, $directory, $tz, $fstype, $error, $preserve_current) = @_;
393              
394 0 0 0       if ($directory ne '.' && $directory ne '..') {
395 0           $directory =~ s{/*\z}{/};
396             }
397              
398 0 0 0       if ( $opts->{use_mlsd}
399             && defined( my $res = _data_command( $self, 'MLSD', $directory ) ) )
400             {
401 0           my @entries = map {
402 0           (my $line = $_) =~ s/(\r\n|\r|\n)\z//;
403              
404 0           my %data;
405 0           my ( $facts, $name ) = split ' ', $line, 2;
406 0           for my $i ( split ';', $facts ) {
407 0           my ( $k, $v ) = split '=', $i, 2;
408 0           $data{lc $k} = $v;
409             }
410              
411             $data{modify}
412 0           =~ /((\d\d)(\d\d\d?))(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/;
413 0 0         my $modify = timegm( $8, $7, $6, $5, $4 - 1,
414             $2 eq '19' ? $3 : ( $1 - 1900 ) );
415              
416 0 0 0       +{ line => '',
    0          
417             data => [
418             $name,
419             ( $data{type} =~ m/dir\z/ ? 'd'
420             : $data{type} =~ m/link/ ? 'l'
421             : 'f'
422             ),
423             $data{size} || 0,
424             $modify,
425             $data{'UNIX.mode'}
426             ],
427             mlsd_facts => \%data,
428             };
429             } split( /\n/, $res );
430              
431 0 0         return wantarray ? @entries : \@entries;
432             }
433             else {
434 0           $opts->{use_mlsd} = 0;
435             }
436              
437 0 0         my $list = _list($self, $directory)
438             or return;
439 0           parse_entries( $list, $tz, $fstype, $error, $preserve_current );
440             }
441              
442             sub parse_entries {
443 0     0 0   my($dir, $tz, $fstype, $error, $preserve_current) = @_;
444              
445 0 0         return unless $dir;
446              
447 0 0         if ($preserve_current) {
448 0           $dir = [ map {
449 0           my $e = $_;
450 0           $e =~ s/(\s\S*)d(?=\S*\z)/$1dd/g;
451 0           $e =~ s/(?<=\s)\.\z/d./g;
452 0           $e;
453             } @$dir ];
454             }
455              
456 0           my @parsed = map {
457 0           my ($data) = File::Listing::parse_dir($_, $tz, $fstype, $error);
458 0 0         $data ? +{ line => $_, data => $data } : ()
459             } @$dir;
460              
461 0 0 0       if (@$dir && ! @parsed) {
462             # Fallback
463 0           @parsed = map {
464 0           my $l = $_;
465 0           $l =~ s/
466             (\s\d+\s+)
467             (\d+)\S*
468             (?=\s+\d+\s+(\d{2}:\d{2}|\d{4}))
469 0           /$1 . $month_name_list[$2-1]/ex;
470 0           my ($data) = File::Listing::parse_dir($l, $tz, $fstype, $error);
471 0 0         $data ? +{ line => $_, data => $data } : ()
472             } @$dir;
473             }
474              
475 0 0         if ($preserve_current) {
476 0           for (@parsed) {
477 0           $_->{data}[0] =~ s/dd/d/;
478 0           $_->{data}[0] =~ s/d\././g;
479             }
480             }
481              
482 0 0         wantarray ? @parsed : \@parsed;
483             }
484              
485             1;
486             __END__