File Coverage

blib/lib/Net/FTP/Simple.pm
Criterion Covered Total %
statement 135 174 77.5
branch 49 106 46.2
condition 6 12 50.0
subroutine 22 24 91.6
pod 4 4 100.0
total 216 320 67.5


line stmt bran cond sub pod time code
1             #
2             # Net::FTP::Simple - Simplified Net::FTP interface encapsulating a few simple
3             # operations.
4             #
5             # Written by Wil Cooley
6             #
7             # $Id: Simple.pm 758 2008-10-11 04:29:18Z wcooley $
8             #
9             package Net::FTP::Simple;
10 2     2   66265 use strict;
  2         5  
  2         64  
11 2     2   10 use warnings;
  2         3  
  2         46  
12 2     2   9 use Carp;
  2         8  
  2         140  
13 2     2   985 use English qw( -no_match_vars );
  2         10299  
  2         13  
14 2     2   932 use File::Basename qw( basename dirname );
  2         4  
  2         159  
15 2     2   9 use File::Spec;
  2         3  
  2         44  
16 2     2   2256 use Net::FTP;
  2         135150  
  2         5347  
17              
18             # FIXME MakeMaker handles it okay if it's all on one line, but Perl::Critic
19             # pukes :(
20             #eval q{ use version; our $VERSION = qv(0.0.5) }; our $VERSION = '0.0005' if ($EVAL_ERROR);
21             our $VERSION = '0.0007';
22              
23              
24             sub send_files {
25             # Allow calls either as Net::FTP::Simple->send_files or
26             # Net::FTP::Simple::send_files
27 5     5 1 3115 my ($opt_ref) = $_[-1];
28              
29 5         9 my @successful_transfers;
30 5         18 my $ftp = Net::FTP::Simple->_new($opt_ref);
31              
32 5 50       18 $ftp->_create_and_cwd_remote_dir()
33             if ($ftp->{'remote_dir'});
34              
35 5         15 FILE_TO_TRANSFER:
36 5         9 for my $file (@{ $ftp->{'files'} }){
37 13         17 my $try_count;
38 13         494 my $basename = basename($file);
39 13         30 my $tmpname = $basename . '.tmp';
40              
41 13 50       258 unless ( -r $file ) {
42 0 0       0 carp $ftp->_error("Local file '$file' unreadable; unable to transfer")
43             unless ($ftp->{'quiet_mode'});
44 0         0 next FILE_TO_TRANSFER;
45             }
46              
47 13 50       38 unless ( $ftp->_conn()->put($file, $tmpname) ) {
48 0 0       0 carp $ftp->_error("Error transferring file '$file' to '$tmpname'")
49             unless ($ftp->{'quiet_mode'});
50 0         0 next FILE_TO_TRANSFER;
51             }
52              
53 13         850 eval {
54 13         38 $try_count = $ftp->_op_retry('rename', $tmpname, $basename);
55             };
56              
57 13 100       117 if ($EVAL_ERROR =~ m/'rename' failed after \d+ attempts/ms) {
    50          
58 2         211 carp "Error renaming '$tmpname' to '$basename'";
59 2         1072 next FILE_TO_TRANSFER;
60             }
61             elsif($EVAL_ERROR) {
62             # Rethrow unexpected exceptions
63 0         0 croak $EVAL_ERROR;
64             }
65              
66 11 100       23 if ($try_count > 1) {
67 4         530 carp "Transfer of file '$file' succeeded after $try_count tries";
68             }
69              
70 11         2856 push @successful_transfers, $file;
71             }
72              
73             wantarray ? return @successful_transfers
74 5 50       51 : return \@successful_transfers
75             ;
76              
77             }
78              
79             sub rename_files {
80             # Allow calls either as Net::FTP::Simple->rename_files or
81             # Net::FTP::Simple::rename_files
82 4     4 1 2363 my ($opt_ref) = $_[-1];
83              
84 4         5 my @successful_renames;
85              
86 4         15 my $ftp = Net::FTP::Simple->_new($opt_ref);
87              
88 4 50       14 if (exists $ftp->{'remote_dir'}) {
89 0 0       0 $ftp->_conn()->cwd($ftp->{'remote_dir'})
90             or croak $ftp->_error("Error changing to remote directory",
91             "'$ftp->{'remote_dir'}'");
92             }
93              
94             FILE_TO_RENAME:
95 4         8 for my $src (sort keys %{ $ftp->{'rename_files'} }) {
  4         23  
96 12         27 my $dst = $ftp->{'rename_files'}{ $src };
97 12         14 my $try_count;
98              
99 12         18 eval {
100 12         29 $try_count = $ftp->_op_retry('rename', $src, $dst);
101             };
102              
103 12 100       148 if ($EVAL_ERROR =~ m/'rename' failed after \d+ attempts/ms) {
    50          
104 4         300 carp "Error renaming '$src' to '$dst'";
105 4         1671 next FILE_TO_RENAME;
106             }
107             elsif ($EVAL_ERROR) {
108             # Rethrow the exception if it's not recognized
109 0         0 croak $EVAL_ERROR;
110             }
111              
112 8 100       21 if ($try_count > 1 ) {
113 4         430 carp "Rename of file from '$src' to '$dst' succeeded after"
114             . " $try_count tries";
115             }
116              
117 8         2317 push @successful_renames, $src;
118              
119             }
120              
121 4         14 @successful_renames = sort @successful_renames;
122              
123             wantarray ? return @successful_renames
124 4 50       34 : return \@successful_renames
125             ;
126              
127             }
128              
129             sub retrieve_files {
130             # Allow calls either as Net::FTP::Simple->retrieve_files or
131             # Net::FTP::Simple::retrieve_files
132 0     0 1 0 my ($opt_ref) = $_[-1];
133              
134 0         0 my @successful_transfers;
135              
136 0         0 my $ftp = Net::FTP::Simple->_new($opt_ref);
137              
138 0 0       0 if ($ftp->{'remote_dir'}) {
139              
140 0 0       0 $ftp->_conn()->cwd($ftp->{'remote_dir'})
141             or croak $ftp->_error("Error changing to remote directory",
142             "'$ftp->{'remote_dir'}'");
143             }
144              
145 0 0       0 if (not exists $ftp->{'files'}) {
146 0 0       0 if (exists $ftp->{'file_filter'}) {
147 0         0 $ftp->_list_and_filter(); # Populate $ftp->{'files'}
148             }
149             else {
150             # Punt if we have neither files nor a file filter
151 0         0 return;
152             }
153             }
154              
155             FILES_TO_TRANSFER:
156 0         0 for my $file (@{ $ftp->{'files'} }) {
  0         0  
157 0         0 my $basename = basename($file);
158              
159 0 0       0 unless ( $ftp->_conn()->get($file, $basename) ) {
160 0 0       0 carp $ftp->_error("Error getting file '$file'")
161             unless ($ftp->{'quiet_mode'});
162 0         0 next FILES_TO_TRANSFER;
163             }
164              
165 0         0 push @successful_transfers, $basename;
166              
167 0 0       0 if ($ftp->{'delete_after'}) {
168 0 0       0 $ftp->_conn()->delete($file)
169             or carp $ftp->_error("Error deleting remote file '$file'");
170             }
171             }
172              
173             wantarray ? return @successful_transfers
174 0 0       0 : return \@successful_transfers
175             ;
176              
177             }
178              
179             sub list_files {
180             # Allow calls either as Net::FTP::Simple->list_files or
181             # Net::FTP::Simple::list_files
182 4     4 1 2134 my ($opt_ref) = $_[-1];
183              
184 4         6 my @remote_files;
185              
186 4         14 my $ftp = Net::FTP::Simple->_new($opt_ref);
187              
188 4 50       15 if ($ftp->{'remote_dir'}) {
189              
190 0 0       0 $ftp->_conn()->cwd($ftp->{'remote_dir'})
191             or croak $ftp->_error("Error changing to remote directory",
192             "'$ftp->{'remote_dir'}'");
193             }
194              
195 4         25 @remote_files = $ftp->_list_and_filter();
196              
197             wantarray ? return @remote_files
198 4 50       151 : return \@remote_files
199             ;
200              
201             }
202              
203             #######################################################################
204             # Private data
205             #######################################################################
206              
207             # Error messages which indicate a possibly temporary error condition
208             our %retryable_errors = (
209             rename => [
210             qq/The process cannot access the file because /
211             . qq/it is being used by another process/,
212             ],
213             );
214              
215             # Maximum number of times to retry an operation.
216             our %retry_max = (
217             default => 0,
218             rename => 3,
219             );
220              
221             # Time to wait on retry in seconds
222             our %retry_wait = (
223             default => 10,
224             rename => 10,
225             );
226              
227              
228             #######################################################################
229             # Private class below here
230             #######################################################################
231             #
232             # Private constructor!
233             #
234             sub _new {
235 22     22   3539 my ($class, $opt_ref) = @_;
236 22         53 my $obj = bless $opt_ref, $class;
237              
238             # Capture which of the wrapper subs called us so we can be identified as
239             # that instead of the actual object.
240 22         134 $obj->_set_caller( (caller(1))[3] );
241              
242             # mmm required options
243 22 100       67 croak $obj->_caller(), " requires at least 'server' parameter"
244             unless($obj->{'server'});
245              
246             #
247             # Allow the user to pass in an object instead of creating a new instance.
248             # This allows test scripts to use a mock Net::FTP object and simulate a
249             # number of different cases.
250             #
251 21 100       62 unless ($obj->{'conn'}) {
252 1 50       10 my $ftpconn = Net::FTP->new($obj->{'server'},
253             Debug => $obj->{'debug_ftp'})
254             or croak $obj->_error("Error creating Net::FTP object:",
255             "'$EVAL_ERROR'");
256              
257 0         0 $obj->_set_conn($ftpconn);
258             }
259             else {
260 20         49 $obj->_set_conn($obj->{'conn'});
261             }
262              
263 20 50       50 $obj->_conn()->login( @{ $obj }{ qw( username password ) } )
  20         124  
264             or croak $obj->_error("Error logging in to '$obj->{'server'}'");
265              
266 20         1318 $obj->_setup_mode();
267              
268 20         1183 return $obj;
269             }
270              
271             sub DESTROY {
272 22     22   4801 my ($self) = shift;
273              
274 22 100       49 if($self->_conn()) {
275 19 50       40 $self->_conn()->quit()
276             or croak $self->_error("Error closing FTP connection:",
277             "'$EVAL_ERROR'");
278             }
279              
280 22         1140 $self->_set_conn(undef);
281             }
282              
283             sub _setup_mode {
284 20     20   38 my ($self) = shift;
285              
286 20 50 33     73 if (exists $self->{'mode'} and $self->{'mode'} eq 'ascii') {
287 0 0       0 $self->_conn()->ascii()
288             or croak $self->_error('Error setting transfer mode to ascii');
289             }
290             else {
291 20 50       44 $self->_conn()->binary()
292             or croak $self->_error('Error setting transfer mode to binary');
293             }
294             }
295              
296             #sub _setup_connection {
297             #my ($self) = shift;
298             #
299             #}
300              
301             #######################################################################
302             # Accessors
303             #######################################################################
304             sub _conn {
305 177     177   239 my ($self) = shift;
306 177         809 return $self->{'connection'};
307             }
308              
309             sub _set_conn {
310 43     43   62 my ($self) = shift;
311 43         53 my ($conn) = @_;
312              
313 43         249 return $self->{'connection'} = $conn;
314             }
315              
316             sub _caller {
317 5     5   9 my ($self) = shift;
318 5         304 return $self->{'caller'};
319             }
320              
321             sub _set_caller {
322 22     22   36 my ($self) = shift;
323 22         39 my ($caller) = @_;
324              
325             # It should always be something; if _new is called directly (as it usually
326             # shouldn't be, but may be in test scripts)
327 22 100       57 $caller = 'main' unless ($caller);
328              
329 22         60 return $self->{'caller'} = $caller;
330             }
331              
332             #
333             # _error - Format the FTP error string to include the error code.
334             #
335             sub _error {
336 2     2   1521 my ($self) = shift;
337 2         5 my ($msg) = join(" ", @_);
338 2         5 my $ftp_err = q{};
339              
340             # This may be called for errors other than those from Net::FTP
341             # and there may not be a connection object
342 2 50 33     5 if ($self->_conn() and not $self->_conn()->ok()) {
343 0         0 my $msg = $self->_conn()->message();
344 0         0 chomp $msg;
345              
346 0         0 $ftp_err = sprintf(q(: '%d %s'), $self->_conn()->code(), $msg);
347             }
348              
349 2         5 return sprintf(q(%s: %s%s),
350             $self->_caller(),
351             $msg,
352             $ftp_err,
353             );
354             }
355              
356              
357             sub _create_and_cwd_remote_dir {
358 0     0   0 my ($self) = shift;
359              
360             # Try first change to the directory
361 0 0       0 unless ($self->_conn()->cwd($self->{'remote_dir'})) {
362              
363             # Give up now if user requested _not_ creating the remote
364             # directory
365 0 0       0 if ($self->{'disable_create_remote_dir'}) {
366 0         0 croak $self->_error("Error changing to remote directory",
367             "'$self->{'remote_dir'}'");
368             }
369              
370             # Try to create the output path if it doesn't exist
371 0 0       0 $self->_conn()->mkdir($self->{'remote_dir'}, 1)
372             or croak $self->_error("Error making remote directory",
373             "'$self->{'remote_dir'}'");
374              
375 0 0       0 $self->_conn()->cwd($self->{'remote_dir'})
376             or croak $self->_error("Error changing to remote directory after",
377             "creating '$self->{'remote_dir'}'");
378             }
379              
380             }
381              
382              
383             sub _op_retry {
384 31     31   1923 my ($self, $op, @op_args) = @_;
385 31         64 my $conn = $self->_conn();
386 31         45 my $try_count = 1;
387              
388 31 50       87 croak ref $conn, " cannot do '$op'"
389             unless ($conn->can($op));
390              
391             OP_TRY:
392 31         735 while(not $conn->$op(@op_args)) {
393 35         2058 $try_count += 1;
394              
395 35 100       82 croak "'$op' failed after $try_count attempts"
396             unless ( $self->_is_retryable_op($op, $try_count) );
397              
398 27         65 $self->_sleep_for_op($op);
399             }
400              
401 23         1589 return $try_count;
402             }
403              
404             #
405             # Sleep for operation; returns nothing useful.
406             #
407             sub _sleep_for_op {
408 27     27   35 my $self = shift;
409 27         35 my ($op) = @_;
410              
411 27 50       69 my $retry_wait = exists $retry_wait{ $op } ? $retry_wait{ $op }
412             : $retry_wait{ 'default' }
413             ;
414              
415 27         153 sleep $retry_wait;
416              
417             }
418              
419             #
420             # _is_retryable_op - Tests if a failing operation is retryable,
421             # comparing both the error message and the retry count.
422             #
423             sub _is_retryable_op {
424 35     35   49 my $self = shift;
425 35         58 my ($op, $count) = @_;
426              
427 35         65 my $caller_error_message = $self->_conn()->message();
428              
429 35 50       1910 my $retry_max = exists $retry_max{ $op } ? $retry_max{ $op }
430             : $retry_max{ 'default' }
431             ;
432              
433 35 50       119 return unless (exists $retryable_errors{ $op });
434 35 100       1027 return if ($count > $retry_max);
435              
436 27         30 for my $msg (@{ $retryable_errors{ $op } }) {
  27         60  
437 27 50       125 if ($caller_error_message =~ m/$msg/ms) { # No 'x'!
438 27         90 return 1;
439             }
440             }
441              
442             # False if we fall through the loop
443 0         0 return;
444             }
445              
446             # Assume everything else is set up
447             sub _list_and_filter {
448 14     14   4115 my ($self) = shift;
449              
450 14   66     98 my $filter = $self->{'file_filter'} || qr/./xms;
451 14         19 my @remote_list;
452             my @remote_files;
453              
454             # FIXME I need to figure out how to distinguish between an empty list (no
455             # files in the directory) and an error. Hopefully, if the directory
456             # permissions are such that the directory cannot be listed, the cwd will
457             # also fail (assuming one is done prior to this :\). Of course, it's
458             # possible to have +x-r, but for now, just hope for the best :)
459 14 100       38 return unless (@remote_list = $self->_conn()->dir());
460              
461             REMOTE_LIST:
462 10         717 for my $entry (@remote_list) {
463 52         121 chomp $entry;
464              
465             # This correctly splits a line where the filename has spaces; the '9'
466             # collects the 9th field and everything after into one item
467 52         196 my ($mode, $filename) = (split /\s+/, $entry, 9)[0,-1];
468              
469             # Skip non-file things
470 52 100 66     379 next REMOTE_LIST unless ($mode && $mode =~ /\A-/xms);
471              
472             # Skip files not matching the filter
473 22 100       123 next REMOTE_LIST unless ($filename =~ m/$filter/xms);
474              
475 19         50 push @remote_files, $filename;
476             }
477              
478 10         16 push @{ $self->{'files'} }, @remote_files;
  10         36  
479              
480             wantarray ? return @remote_files
481 10 100       74 : return \@remote_files
482             ;
483             }
484              
485              
486              
487             1;
488              
489             __END__