File Coverage

lib/Net/FTP/Robust.pm
Criterion Covered Total %
statement 24 149 16.1
branch 0 70 0.0
condition 0 36 0.0
subroutine 8 20 40.0
pod 0 4 0.0
total 32 279 11.4


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