File Coverage

lib/Net/FTP/Robust.pm
Criterion Covered Total %
statement 24 150 16.0
branch 0 70 0.0
condition 0 45 0.0
subroutine 8 20 40.0
pod 2 4 50.0
total 34 289 11.7


line stmt bran cond sub pod time code
1             # Copyrights 2009-2018 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Net-FTP-Robust. Meta-POD processed
6             # with OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Net::FTP::Robust;
10 1     1   790 use vars '$VERSION';
  1         2  
  1         55  
11             $VERSION = '0.09';
12              
13              
14 1     1   5 use warnings;
  1         2  
  1         20  
15 1     1   3 use strict;
  1         1  
  1         28  
16              
17 1     1   399 use Log::Report 'net-ftp-robust', syntax => 'SHORT';
  1         91400  
  1         5  
18 1     1   788 use Net::FTP;
  1         71806  
  1         63  
19 1     1   484 use Time::HiRes qw/gettimeofday tv_interval/;
  1         1109  
  1         3  
20              
21 1     1   705 use Data::Dumper;
  1         5028  
  1         76  
22              
23             sub size_short($);
24             use constant
25 1         1440 { GB => 1024 * 1024 * 1024
26             , MB => 1024 * 1024
27             , kB => 1024
28 1     1   6 };
  1         2  
29              
30              
31 0     0 1   sub new() { my $class = shift; (bless {}, $class)->init( {@_} ) }
  0            
32              
33             sub init($)
34 0     0 0   { my ($self, $args) = @_;
35              
36             # delete all my own options from the %$args
37             $self->{login_attempts}
38 0 0         = defined $args->{login_attempts} ? delete $args->{login_attempts} : 10;
39              
40             # probably, some people will attempt lowercased 'host'
41 0   0       $args->{Host} ||= delete $args->{host};
42              
43 0   0       $self->{login_user} = delete $args->{user} || 'anonymous';
44 0   0       $self->{login_password} = delete $args->{password} || '-anonymous@';
45 0   0       $self->{login_delay} = delete $args->{login_delay} || 60;
46              
47             $self->{skip_names} = delete $args->{skip_names}
48 0   0 0     || sub { $_[2] =~ m/^\./ }; # UNIX hidden files
  0            
49              
50 0           $self->{ftp_opts} = $args;
51 0           $self;
52             }
53              
54              
55             sub _connect($)
56 0     0     { my ($self, $opts) = @_;
57 0           my $ftp = Net::FTP->new(%$opts);
58 0 0         my $err = defined $ftp ? undef : $@;
59 0           ($ftp, $err);
60             }
61              
62             sub get($$)
63 0     0 1   { my ($self, $from, $to) = @_;
64              
65 0 0 0       $to = File::Spec->curdir
66             unless defined $to && length $to;
67 0           $from =~ s,^/?,/,g; # ensure leading /
68              
69 0   0       my $retries = $self->{login_attempts} || 1_000_000;
70 0           my $success = 0;
71              
72             ATTEMPT: # see continue block at end
73 0           foreach my $attempt (1..$retries)
74 0 0         { info __x"connection attempt {nr}{max}"
    0          
75             , nr => $attempt, max => ($retries ? " of $retries" : '')
76             if $attempt != 1;
77              
78 0           my ($ftp, $err) = $self->_connect($self->{ftp_opts});
79 0 0         unless($ftp)
80 0           { notice __x"cannot establish contact: {err}", err => $err;
81 0           next ATTEMPT;
82             }
83              
84 0 0         unless( $ftp->login($self->{login_user}, $self->{login_password}))
85 0   0       { notice __x"login failed: {msg}", msg => ($ftp->message || $!);
86 0           next ATTEMPT;
87             }
88              
89 0           $ftp->binary;
90 0           my ($dir, $base) = $from =~ m!^(?:(.*)/)?([^/]*)!;
91 0   0       $dir ||= '/';
92 0 0         unless($ftp->cwd($dir))
93 0   0       { notice __x"directory {dir} does not exist: {msg}"
94             , dir => $dir, msg => ($ftp->message || $!);
95 0           next ATTEMPT;
96             }
97              
98             my $stats = $self->{stats}
99 0           = { files => 0, new_files => 0, downloaded => 0 };
100 0           my $start = [ gettimeofday ];
101 0           $success = $self->_recurse($ftp, $dir, $base, $to);
102 0           my $elapsed = tv_interval $start;
103              
104 0 0         $success
105             or notice __x"attempt {nr} unsuccessful", nr => $attempt;
106              
107             info __x"Got {new} new files, {size} in {secs}s avg {speed}/s"
108             , new => $stats->{new_files}
109             , total => $stats->{files}
110             , size => size_short($stats->{downloaded})
111             , secs => int($elapsed)
112 0           , speed => size_short($stats->{downloaded} / $elapsed);
113              
114 0           $ftp->close;
115              
116 0 0         last if $success;
117             }
118             continue
119 0           { sleep $self->{login_delay};
120             }
121              
122 0           $success;
123             }
124              
125             sub _recurse($$$$)
126 0     0     { my ($self, $ftp, $dir, $entry, $to) = @_;
127              
128 0           my $full = $dir . $entry;
129 0 0         if($self->{skip_names}->($ftp, $full, $entry))
130 0           { trace "skipping $full";
131 0           return 1;
132             }
133              
134 0 0         if(!length $entry)
    0          
135 0 0 0       { -d $to || mkdir $to
136             or fault __x"cannot create directory {dir}", dir => $to;
137              
138 0           return $self->_get_directory($ftp, $dir, $to);
139             }
140             elsif($ftp->cwd($entry))
141             { # Entering directory
142 0           $to = File::Spec->catdir($to, $entry);
143            
144 0 0 0       -d $to || mkdir $to
145             or fault __x"cannot create directory {dir}", dir => $to;
146              
147 0 0         $full .= '/' if $full ne '/';
148 0           my $success = $self->_get_directory($ftp, $full, $to);
149 0 0         if($success)
150 0 0 0       { $success = $ftp->cdup
151             or notice __x"cannot go cdup to {dir}: {msg}"
152             , dir => $dir, msg => ($ftp->message || $!);
153             }
154 0           return $success;
155             }
156              
157 0           $self->_get_file($ftp, $dir, $entry, $to);
158             }
159              
160 0     0     sub _ls($) { $_[1]->ls }
161              
162             sub _get_directory($$$)
163 0     0     { my ($self, $ftp, $where, $to) = @_;
164 0           my @entries = $self->_ls($ftp);
165              
166 0           trace "directory $where has ".@entries. " entries";
167              
168 0           foreach my $entry (@entries)
169 0           { my $success = $self->_recurse($ftp, $where, $entry, $to);
170 0 0         $success or return 0;
171             }
172              
173 0           1;
174             }
175              
176             # Different in Net::FTPSSL
177             sub _modif_time($$)
178 0     0     { my ($self, $ftp, $fn) = @_;
179 0 0         $ftp->mdtm($fn) || 0;
180             }
181            
182             sub _can_restart($$$$)
183 0     0     { my ($self, $ftp, $name, $temp, $expected_size) = @_;
184 0   0       my $got_size = -s $temp || 0;
185 0 0         $got_size or return 0;
186              
187             # download did not complete last time
188 0           my $to_download = $expected_size - $got_size;
189 0           info "continue file $name, got " . size_short($got_size)
190             . " from " . size_short($expected_size)
191             . ", needs " . size_short($to_download);
192              
193 0           $ftp->restart($got_size);
194 0           $got_size;
195             }
196              
197             sub _get_file($$$$)
198 0     0     { my ($self, $ftp, $dir, $base, $to) = @_;
199              
200 0           my $remote_name = $dir . $base;
201 0           my $local_name = "$to/$base";
202 0           my $local_temp = "$to/.$base";
203              
204 0           my $remote_mtime = $self->_modif_time($ftp, $base);
205 0           my $stats = $self->{stats};
206 0           $stats->{files}++;
207              
208 0 0         if(-e $local_name)
209             { # file already downloaded, still valid?
210 0 0         if(! -f $local_name)
211             { # not downloadable
212 0           notice __x"download file {fn}, but already exists as non-file"
213             , fn => $local_name;
214 0           return 1;
215             }
216              
217 0           my $local_mtime = (stat $to)[9];
218 0 0 0       if($remote_mtime && $local_mtime >= $remote_mtime)
219 0           { trace "file $remote_name already downloaded";
220 0           return 1;
221             }
222              
223 0           trace "local file $local_name is outdated";
224             # continue as if the file does not exist
225             }
226              
227 0           my $expected_size = $ftp->size($base);
228 0 0         my $got_size
229             = $self->_can_restart($ftp, $local_name, $local_temp, $expected_size)
230             or trace "get " . size_short($expected_size). " for $local_name";
231            
232 0           my $success;
233 0 0 0       if(defined $expected_size && $expected_size==$got_size)
234             { # download succesful, but mv or close was not
235 0           $success = 1;
236 0 0         if($expected_size==0)
237 0 0         { open OUT, '>', $local_temp
238             or fault __x"cannot create empty {file}", file => $local_temp;
239 0           close OUT;
240             }
241             }
242             else
243 0           { my $start = [ gettimeofday ];
244 0           $success = $ftp->get($base, $local_temp);
245 0           my $elapsed = tv_interval $start;
246              
247 0   0       my $downloaded = (-s $local_temp || 0) - $got_size;
248              
249 0 0         if($downloaded)
250 0           { info __x"{amount} in {secs}s is {speed}/s: {fn}"
251             , amount => size_short($downloaded)
252             , secs => sprintf("%7.3f", $elapsed)
253             , speed => size_short($downloaded/$elapsed), fn => $base;
254 0           $stats->{downloaded} += $downloaded;
255             }
256             else
257 0           { notice __x"failed to get any bytes from {fn}: {err}"
258             , fn => $local_name, err => $ftp->message;
259 0           $success = 0;
260             }
261             }
262              
263 0 0         if($success)
264             { # accept the downloaded file
265 0           utime $remote_mtime, $remote_mtime, $local_temp; # only root
266 0           unlink $local_name; # might exist
267 0 0         unless(rename $local_temp, $local_name)
268 0           { fault __x"cannot rename {old} to {new}"
269             , old => $local_temp, new => $local_name;
270             }
271 0           $stats->{new_files}++;
272             }
273              
274 0           $success;
275             }
276              
277             sub size_short($)
278 0   0 0 0   { my $size = shift || 0;
279 0           my $name = ' B';
280 0 0         ($size, $name) = ($size/1024, 'kB') if $size > 1000;
281 0 0         ($size, $name) = ($size/1024, 'MB') if $size > 1000;
282 0 0         ($size, $name) = ($size/1024, 'GB') if $size > 1000;
283              
284 0 0         my $format = $size >= 100 ? "%4.0f%s" : "%4.1f%s";
285 0           sprintf $format, $size, $name;
286             }
287              
288              
289             1;