File Coverage

blib/lib/Net/Async/FTP.pm
Criterion Covered Total %
statement 289 306 94.4
branch 133 168 79.1
condition 41 65 63.0
subroutine 42 53 79.2
pod 13 15 86.6
total 518 607 85.3


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2008-2013 -- leonerd@leonerd.org.uk
5              
6             package Net::Async::FTP;
7              
8 8     8   1059673 use strict;
  8         19  
  8         479  
9 8     8   48 use warnings;
  8         14  
  8         273  
10 8     8   52 use base qw( IO::Async::Stream );
  8         15  
  8         2027  
11             IO::Async::Stream->VERSION( '0.59' );
12              
13 8     8   120919 use Carp;
  8         17  
  8         872  
14              
15             our $VERSION = '0.08';
16              
17 8     8   43 use Socket qw( AF_INET SOCK_STREAM inet_aton pack_sockaddr_in );
  8         15  
  8         42193  
18              
19             my $CRLF = "\x0d\x0a";
20              
21             =head1 NAME
22              
23             C - use FTP with C
24              
25             =head1 SYNOPSIS
26              
27             use IO::Async::Loop;
28             use Net::Async::FTP;
29              
30             my $loop = IO::Async::Loop->new();
31              
32             my $ftp = Net::Async::FTP->new();
33             $loop->add( $ftp );
34              
35             $ftp->connect(
36             host => "ftp.example.com",
37             )->then( sub {
38             $ftp->login(
39             user => "username",
40             pass => "password",
41             )
42             })->then( sub {
43             $ftp->retr(
44             path => "README.txt",
45             )
46             })->then( sub {
47             my ( $data ) = @_;
48             print "README.txt says:\n";
49             print $data;
50             })->get;
51              
52             =head1 DESCRIPTION
53              
54             This object class implements an asynchronous FTP client, for use in
55             L-based programs.
56              
57             The code in this module is not particularly complete. It contains a minimal
58             implementation of a few FTP commands, not even the full minimal set the RFC
59             suggests all clients should support. I am releasing it anyway, because it is
60             still useful as it stands, and could easily support extra commands being added
61             if anyone would find it useful.
62              
63             The (undocumented) C method provides a generic base for the
64             currently-implemented commands, and would be the basis for new commands.
65              
66             As they say so often in the open-source world; Patches Welcome.
67              
68             =cut
69              
70             =head1 CONSTRUCTOR
71              
72             =cut
73              
74             =head2 $ftp = Net::Async::FTP->new( %args )
75              
76             This function returns a new instance of a C object. As it is
77             a subclass of C its constructor takes any arguments for
78             that class.
79              
80             =cut
81              
82             sub new
83             {
84 8     8 1 53277 my $class = shift;
85 8         29 my %args = @_;
86              
87 8         127 my $self = $class->SUPER::new( %args );
88              
89 8         2200 $self->{req_queue} = [];
90              
91 8         30 return $self;
92             }
93              
94             sub on_read
95             {
96 59     59 1 82722 my $self = shift;
97 59         124 my ( $buffref, $closed ) = @_;
98              
99 59         183 $self->_do_req_queue;
100              
101 59 100       103 if( my $item = shift @{ $self->{req_queue} } ) {
  59         212  
102 32         204 return $item->{on_read};
103             }
104              
105 27 50       341 return 0 unless $$buffref =~ s/^(.*)$CRLF//;
106 0         0 print STDERR "Unexpected incoming line $1\n";
107 0         0 return 1;
108             }
109              
110             =head1 METHODS
111              
112             =cut
113              
114             =head2 $ftp->connect( %args ) ==> ()
115              
116             Connects to the FTP server. Takes the following arguments:
117              
118             =over 8
119              
120             =item host => STRING
121              
122             Hostname of the server
123              
124             =item service => STRING or INT
125              
126             Optional. Service name or port number to connect to. If not supplied, will use
127             C.
128              
129             =item family => INT
130              
131             Optional. Socket family to use. Will default to whatever C
132             returns if not supplied.
133              
134             =item on_connected => CODE
135              
136             Optional when returning a Future. Continuation to call when connection is
137             successful.
138              
139             $on_connected->()
140              
141             =item on_error => CODE
142              
143             Optional when returning a Future. Continuation to call on an error.
144              
145             $on_error->( $message )
146              
147             =back
148              
149             =cut
150              
151             sub connect
152             {
153 2     2 1 348 my $self = shift;
154 2         20 my %args = @_;
155              
156 2 50 66     17 my $on_connected = $args{on_connected} or defined wantarray or croak "Expected 'on_connected'";
157 2 50 66     17 my $on_error = $args{on_error} or defined wantarray or croak "Expected 'on_error'";
158              
159             my $f = $self->SUPER::connect(
160             service => "ftp",
161             %args,
162             )->then( sub {
163             # TODO: This is a bit messy. Install an initial on_read handler for
164             # the connect messages, by sending an "empty string" command
165 2     2   2116496 $self->do_command( undef, [ 220 ] );
166 2         21 });
167              
168 2 100       59248 $f->on_done( $on_connected ) if $on_connected;
169 2 100       17 $f->on_fail( $on_error ) if $on_error;
170              
171 2 100       34 return $f if defined wantarray;
172 1     0   7 $f->on_ready( sub { undef $f } ); # Intentional cycle
  0         0  
173             }
174              
175             my %NUMTYPES = (
176             1 => "info",
177             2 => "ok",
178             3 => "more",
179             4 => "err",
180             5 => "err",
181             );
182              
183             sub _build_future_onread
184             {
185 45     45   88 my $self = shift;
186 45         106 my ( $command, $f, $accept, %continue ) = @_;
187              
188 45         62 my @extralines;
189 45         110 my %accept = map { $_ => 1 } @$accept;
  33         177  
190              
191             sub {
192 83     83   28771 my ( $self, $buffref, $closed ) = @_;
193              
194 83 100       913 return 0 unless $$buffref =~ s/^(.*)$CRLF//;
195 66         243 my $line = $1;
196              
197 66 100       368 if( $line =~ m/^(\d{3}) +(.*)$/ ) {
    50          
198 46         149 my ( $number, $message ) = ( $1, $2 );
199 46         155 my $numtype = $NUMTYPES{substr($number, 0, 1)};
200              
201 46 100 100     260 if( $accept{$number} || $accept{$numtype} ) {
    50          
    0          
202 32 50       295 if( $numtype eq "info" ) {
203 0         0 print STDERR "TODO: info $number\n";
204             }
205             else {
206 32         155 $f->done( $number, $message, @extralines );
207 32         2007 return undef;
208             }
209             }
210             elsif( my $cb = $continue{$number} ) {
211 14         44 return $cb->( $f, $number, $message );
212             }
213             elsif( $numtype eq "err" ) {
214 0         0 $f->fail( $message, ftp => $number );
215             }
216             else {
217 0         0 print STDERR "Unexpected incoming $number\n";
218             }
219             }
220             elsif( $line =~ m/^(\d{3})-(.*)$/ ) {
221 20         52 push @extralines, $2;
222             }
223             else {
224 0         0 print STDERR "Unparseable incoming line $line\n";
225             }
226              
227 20         60 return 1;
228 45         346 };
229             }
230              
231             sub do_command
232             {
233 33     33 0 57 my $self = shift;
234 33         108 my ( $command, $accept, %continue ) = @_;
235              
236 33         142 my $f = $self->loop->new_future;
237 33         10616 my $on_read = $self->_build_future_onread( $command, $f, $accept, %continue );
238              
239 33         69 my $queue = $self->{req_queue};
240 33         133 push @$queue, { command => $command, on_read => $on_read };
241              
242 33         96 $self->_do_req_queue;
243              
244 33         220 return $f;
245             }
246              
247             sub _do_req_queue
248             {
249 92     92   135 my $self = shift;
250              
251 92         149 my $queue = $self->{req_queue};
252 92 100       247 return unless @$queue;
253              
254 65         242 my $item = $queue->[0];
255              
256 65 100       198 if( defined $item->{command} ) {
257 31         223 $self->write( "$item->{command}$CRLF" );
258 31         4634 undef $item->{command};
259             }
260             }
261              
262             sub _connect_dataconn
263             {
264 12     12   32 my $self = shift;
265 12         30 my ( $on_conn ) = @_;
266              
267             $self->do_command( "PASV", [],
268             227 => sub {
269 12     12   30 my ( $f, $num, $message ) = @_;
270 12 50       80 $message =~ m/\((\d+,\d+,\d+,\d+,\d+,\d+)\)/ or
271             return $f->fail( "Did not find (ip,port) in message $message", ftp => $num, $message );
272              
273 12         77 my ( $ipA, $ipB, $ipC, $ipD, $portHI, $portLO ) = split( m/,/, $1 );
274 12         56 my $ip = "$ipA.$ipB.$ipC.$ipD";
275 12         40 my $port = $portHI*256 + $portLO;
276              
277 12         66 my $sinaddr = pack_sockaddr_in( $port, inet_aton( $ip ) );
278              
279 12         72 my $loop = $self->get_loop;
280 12         133 my $connect_f = $loop->connect(
281             addr => [ AF_INET, SOCK_STREAM, 0, $sinaddr ],
282             );
283 12         59810 $connect_f->on_fail( $f );
284              
285 12         115 return $on_conn->( $f, $connect_f );
286             },
287 12         110 );
288             }
289              
290             # Now some convenient wrappers for classes of command
291              
292             sub _do_command_collect_dataconn
293             {
294 10     10   18 my $self = shift;
295 10         16 my ( $command ) = @_;
296              
297             $self->_connect_dataconn(
298             sub {
299 10     10   18 my ( $f, $connect_f ) = @_;
300              
301 10         15 my $data;
302             my $dataconn = IO::Async::Stream->new(
303             on_read => sub {
304 20         20843 my ( $self, $buffref, $closed ) = @_;
305 20 100       91 return 0 unless $closed;
306 10         23 $data = $$buffref;
307 10         37 $self->close;
308 10         5683 return 0;
309             },
310 10         2163 );
311 10         1067 $self->add_child( $dataconn );
312              
313             $connect_f->on_done( sub {
314 10         11272 $dataconn->configure( read_handle => $_[0] );
315 10         1045 });
316 10         131 $connect_f->on_ready( sub { undef $connect_f } ); # Intentional cycle
  10         5666  
317              
318 10         117 $self->write( "$command$CRLF" );
319              
320 10         1213 my $cmd_f = $f->new;
321 10         177 my $on_read = $self->_build_future_onread( $command, $cmd_f, [ 226 ] );
322              
323             my $done_f = Future->needs_all( $dataconn->new_close_future, $cmd_f )
324 10         3180 ->on_done( sub { $f->done( $data ) })
325 10         58 ->on_fail( $f );
326 10         1698 $f->on_cancel( $done_f );
327              
328 10         121 return $on_read;
329             },
330 10         87 );
331             }
332              
333             sub _do_command_send_dataconn
334             {
335 2     2   5 my $self = shift;
336 2         12 my ( $command, $data ) = @_;
337              
338             $self->_connect_dataconn(
339             sub {
340 2     2   4 my ( $f, $connect_f ) = @_;
341              
342 2         25 my $dataconn = IO::Async::Stream->new;
343 2         394 $self->add_child( $dataconn );
344              
345             $connect_f->on_done( sub {
346 2         4528 $dataconn->configure( write_handle => $_[0] );
347 2         266 });
348 2         28 $connect_f->on_ready( sub { undef $connect_f } ); # Intentional cycle
  2         186  
349              
350 2         27 $self->write( "$command$CRLF" );
351              
352 2         422 my $cmd_f = $f->new;
353             my $on_read = $self->_build_future_onread( $command, $cmd_f, [ 226 ],
354             150 => sub {
355 2         12 $dataconn->write( $data );
356 2         349 $dataconn->close_when_empty;
357 2         27 return 1;
358             },
359 2         45 );
360              
361             my $done_f = Future->needs_all( $dataconn->new_close_future, $cmd_f )
362 2         1002 ->on_done( sub { $f->done } )
363 2         16 ->on_fail( $f );
364 2         323 $f->on_cancel( $done_f );
365              
366 2         28 return $on_read;
367             },
368 2         19 );
369             }
370              
371             =head2 $ftp->login( %args ) ==> ()
372              
373             Sends a C and optionally C command. Takes the following arguments:
374              
375             =over 8
376              
377             =item user => STRING
378              
379             Username for the C command
380              
381             =item pass => STRING
382              
383             Password for the C command if required
384              
385             =item on_login => CODE
386              
387             Optional when returning a future. Continuation to invoke on successful login.
388              
389             $on_login->()
390              
391             =item on_error => CODE
392              
393             Optional when returning a future. Continuation to invoke on an error.
394              
395             $on_error->( $message )
396              
397             =back
398              
399             =cut
400              
401             sub login
402             {
403 3     3 1 1404 my $self = shift;
404 3         23 my %args = @_;
405              
406 3 50       15 my $user = $args{user} or croak "Expected 'user'";
407              
408 3 50 66     35 my $on_login = $args{on_login} or defined wantarray or croak "Expected 'on_login'";
409 3 50 66     35 my $on_error = $args{on_error} or defined wantarray or croak "Expected 'on_error'";
410              
411             my $f = $self->do_command( "USER $user", [ 331 ] )
412             ->then( sub {
413 3 50   3   162 exists $args{pass} or return $on_error->( "No password" );
414 3         18 $self->do_command( "PASS $args{pass}", [ 230 ] )
415 3         21 });
416              
417 3 100       213 $f->on_done( $on_login ) if $on_login;
418 3 100       22 $f->on_fail( $on_error ) if $on_error;
419              
420 3 100       21 return $f if defined wantarray;
421 1     1   10 $f->on_ready( sub { undef $f } ); # Intentional cycle
  1         70  
422             }
423              
424             =head2 $ftp->rename( %args ) ==> ()
425              
426             Renames a file on the remote server. Takes the following arguments
427              
428             =over 8
429              
430             =item oldpath => STRING
431              
432             Path to file to rename
433              
434             =item newpath => STRING
435              
436             Desired new path for the file
437              
438             =item on_done => CODE
439              
440             Optional when returning a future. Continuation to invoke on success.
441              
442             $on_done->()
443              
444             =item on_error => CODE
445              
446             Optional. Continuation to invoke on an error.
447              
448             $on_error->( $message )
449              
450             =back
451              
452             =cut
453              
454             sub rename
455             {
456 2     2 1 1081 my $self = shift;
457 2         8 my %args = @_;
458              
459 2         4 my $oldpath = $args{oldpath};
460 2 50       6 defined $oldpath or croak "Expected 'oldpath'";
461              
462 2         5 my $newpath = $args{newpath};
463 2 50       12 defined $newpath or croak "Expected 'newpath'";
464              
465 2 50 66     11 my $on_done = $args{on_done} or defined wantarray or croak "Expected 'on_done'";
466              
467 2         3 my $on_error = $args{on_error};
468 2 100 50 0   14 $on_error ||= sub { die "Error $_[0] during rename" } if !defined wantarray;
  0         0  
469              
470             my $f = $self->do_command( "RNFR $oldpath", [ 350 ] )
471             ->then( sub {
472 2     2   113 $self->do_command( "RNTO $newpath", [ 'ok' ] )
473 2         10 });
474              
475 2 100       156 $f->on_done( $on_done ) if $on_done;
476 2 100       19 $f->on_fail( $on_error ) if $on_error;
477              
478 2 100       15 return $f if defined wantarray;
479 1     1   10 $f->on_ready( sub { undef $f } ); # Intentional cycle
  1         90  
480             }
481              
482             =head2 $ftp->dele( %args ) ==> ()
483              
484             Deletes a file on the remote server. Takes the following arguments
485              
486             =over 8
487              
488             =item path => STRING
489              
490             Path to file to delete
491              
492             =item on_done => CODE
493              
494             Optional when returning a future. Continuation to invoke on success.
495              
496             $on_done->()
497              
498             =item on_error => CODE
499              
500             Optional. Continuation to invoke on an error.
501              
502             $on_error->( $message )
503              
504             =back
505              
506             =cut
507              
508             sub dele
509             {
510 2     2 1 792 my $self = shift;
511 2         9 my %args = @_;
512              
513 2         5 my $path = $args{path};
514 2 50       7 defined $path or croak "Expected 'path'";
515              
516 2 50 66     16 my $on_done = $args{on_done} or defined wantarray or croak "Expected 'on_done'";
517              
518 2         5 my $on_error = $args{on_error};
519 2 100 50 0   13 $on_error ||= sub { die "Error $_[0] during RETR" } if !defined wantarray;
  0         0  
520              
521 2         13 my $f = $self->do_command( "DELE $path", [ 'ok' ] );
522              
523 2 100       15 $f->on_done( $on_done ) if $on_done;
524 2 100       24 $f->on_fail( $on_error ) if $on_error;
525              
526 2         18 return $f;
527             }
528              
529             =head2 $ftp->list( %args ) ==> $list
530              
531             Runs a C command on a path on the remote server; which requests details
532             on the file, or contents of the directory. Takes the following arguments
533              
534             =over 8
535              
536             =item path => STRING
537              
538             Path to C
539              
540             =item on_list => CODE
541              
542             Optional when returning a future. Continuation to invoke on success. Is passed
543             a list of lines from the C result in a single string.
544              
545             $on_list->( $list )
546              
547             =item on_error => CODE
548              
549             Optional. Continuation to invoke on an error.
550              
551             $on_error->( $message )
552              
553             =back
554              
555             The C method may be easier to use as it parses the lines.
556              
557             =cut
558              
559             sub list
560             {
561 4     4 1 1107 my $self = shift;
562 4         22 my %args = @_;
563              
564 4         8 my $path = $args{path};
565              
566 4 50 66     26 my $on_list = $args{on_list} or defined wantarray or croak "Expected 'on_list'";
567              
568 4         8 my $on_error = $args{on_error};
569 4 100 50 0   26 $on_error ||= sub { die "Error $_[0] during LIST" } if !defined wantarray;
  0         0  
570              
571 4 50       25 my $f = $self->_do_command_collect_dataconn(
572             "LIST" . ( defined $path ? " $path" : "" ),
573             );
574              
575 4 100       35 $f->on_done( $on_list ) if $on_list;
576 4 100       19 $f->on_fail( $on_error ) if $on_error;
577              
578 4         29 return $f;
579             }
580              
581             =head2 $ftp->list_parsed( %args ) ==> @list
582              
583             Runs a C command on a path on the remote server; and parse the result
584             lines. Takes the following arguments
585              
586             =over 8
587              
588             =item path => STRING
589              
590             Path to C
591              
592             =item on_list => CODE
593              
594             Optional when returning a future. Continuation to invoke on success. Is passed
595             a list of files from the C result, one line per element.
596              
597             $on_list->( @list )
598              
599             =item on_error => CODE
600              
601             Optional. Continuation to invoke on an error.
602              
603             $on_error->( $message )
604              
605             =back
606              
607             The C<@list> array will be passed a list of C references, each formed
608             like
609              
610             =over 8
611              
612             =item name => STRING
613              
614             The filename
615              
616             =item type => STRING
617              
618             A single character; C for files, C for directories
619              
620             =item size => INT
621              
622             The size in bytes
623              
624             =item mtime => INT
625              
626             The item's last modify timestamp, as a UNIX epoch time
627              
628             =item mode => INT
629              
630             The access mode, as a number
631              
632             =back
633              
634             =cut
635              
636             sub list_parsed
637             {
638 2     2 1 1967 my $self = shift;
639 2         7 my %args = @_;
640              
641 2 50 66     16 my $on_list = $args{on_list} or defined wantarray or croak "Expected 'on_list'";
642              
643 2         4 my $on_error = $args{on_error};
644 2 100 50 0   14 $on_error ||= sub { die "Error $_[0] during LIST" } if !defined wantarray;
  0         0  
645              
646 2         1149 require File::Listing;
647              
648             my $f = $self->list(
649             path => $args{path},
650             )->then( sub {
651 2     2   84 my ( $list ) = @_;
652 2         10 my @files = File::Listing::parse_dir( $list );
653              
654             # We want to present a list of HASH refs, as they're nicer to work with
655 2         844 @files = map { my %h; @h{qw( name type size mtime mode )} = @$_; \%h } @files;
  4         6  
  4         20  
  4         13  
656              
657 2         14 return Future->new->done( @files );
658 2         7976 });
659              
660 2 100       133 $f->on_done( $on_list ) if $on_list;
661 2 100       14 $f->on_fail( $on_error ) if $on_error;
662              
663 2 100       20 return $f if defined wantarray;
664 1     1   8 $f->on_ready( sub { undef $f } ); # Intentional cycle
  1         132  
665             }
666              
667             =head2 $ftp->nlist( %args ) ==> $list
668              
669             Runs a C command on a path on the remote server; which requests a list
670             of filenames in a directory. Takes the following arguments
671              
672             =over 8
673              
674             =item path => STRING
675              
676             Path to C
677              
678             =item on_list => CODE
679              
680             Optional when returning a future. Continuation to invoke on success. Is passed
681             a list of names from the C result in a single string.
682              
683             $on_list->( $list )
684              
685             =item on_error => CODE
686              
687             Optional. Continuation to invoke on an error.
688              
689             $on_error->( $message )
690              
691             =back
692              
693             The C method may be easier to use as it splits the lines.
694              
695             =cut
696              
697             sub nlst
698             {
699 4     4 0 2930 my $self = shift;
700 4         11 my %args = @_;
701              
702 4         10 my $path = $args{path};
703              
704 4 50 66     24 my $on_list = $args{on_list} or defined wantarray or croak "Expected 'on_list'";
705              
706 4         9 my $on_error = $args{on_error};
707 4 100 50 0   19 $on_error ||= sub { die "Error $_[0] during NLST" } if !defined wantarray;
  0         0  
708              
709 4 50       24 my $f = $self->_do_command_collect_dataconn(
710             "NLST" . ( defined $path ? " $path" : "" ),
711             );
712              
713 4 100       19 $f->on_done( $on_list ) if $on_list;
714 4 100       18 $f->on_fail( $on_error ) if $on_error;
715              
716 4         30 return $f;
717             }
718              
719             =head2 $ftp->namelist( %args ) ==> @names
720              
721             Runs a C command on a path on the remote server; which requests a list
722             of filenames in a directory. Takes the following arguments
723              
724             =over 8
725              
726             =item path => STRING
727              
728             Path to C
729              
730             =item on_names => CODE
731              
732             Optional when returning a future. Continuation to invoke on success. Is passed
733             a list of names from the C result in a list, one name per entry
734              
735             $on_name->( @names )
736              
737             =item on_error => CODE
738              
739             Optional. Continuation to invoke on an error.
740              
741             $on_error->( $message )
742              
743             =back
744              
745             =cut
746              
747             sub namelist
748             {
749 2     2 1 2065 my $self = shift;
750 2         36 my %args = @_;
751              
752 2 50 66     15 my $on_names = $args{on_names} or defined wantarray or croak "Expected 'on_names'";
753              
754 2         3 my $on_error = $args{on_error};
755 2 100 50 0   16 $on_error ||= sub { die "Error $_[0] during NLST" } if !defined wantarray;
  0         0  
756              
757             my $f = $self->nlst(
758             path => $args{path},
759             )->then( sub {
760 2     2   91 my ( $list ) = @_;
761 2         13 return Future->new->done( split( m/\r?\n/, $list ) );
762 2         12 });
763              
764 2 100       132 $f->on_done( $on_names ) if $on_names;
765 2 100       14 $f->on_fail( $on_error ) if $on_error;
766              
767 2 100       15 return $f if defined wantarray;
768 1     1   7 $f->on_ready( sub { undef $f } ); # Intentional cycle
  1         122  
769             }
770              
771             =head2 $ftp->retr( %args ) ==> $content
772              
773             Retrieves a file on the remote server. Takes the following arguments
774              
775             =over 8
776              
777             =item path => STRING
778              
779             Path to file to retrieve
780              
781             =item on_data => CODE
782              
783             Optional when returning a future. Continuation to invoke on success. Is
784             passed the contents of the file as a single string.
785              
786             $on_data->( $content )
787              
788             =item on_error => CODE
789              
790             Optional. Continuation to invoke on an error.
791              
792             $on_error->( $message )
793              
794             =back
795              
796             =cut
797              
798             sub retr
799             {
800 2     2 1 4006 my $self = shift;
801 2         11 my %args = @_;
802              
803 2         4 my $path = $args{path};
804 2 50       12 defined $path or croak "Expected 'path'";
805              
806 2 50 66     16 my $on_data = $args{on_data} or defined wantarray or croak "Expected 'on_data' as CODE reference";
807              
808 2         5 my $on_error = $args{on_error};
809 2 100 50 0   16 $on_error ||= sub { die "Error $_[0] during RETR" } if !defined wantarray;
  0         0  
810              
811 2         12 my $f = $self->_do_command_collect_dataconn(
812             "RETR $path",
813             );
814              
815 2 100       13 $f->on_done( $on_data ) if $on_data;
816 2 100       18 $f->on_fail( $on_error ) if $on_error;
817              
818 2         15 return $f;
819             }
820              
821             =head2 $ftp->stat( %args ) ==> @stat
822              
823             Runs a C command on a path on the remote server; which requests details
824             on the file, or contents of the directory. Takes the following arguments
825              
826             =over 8
827              
828             =item path => STRING
829              
830             Path to C
831              
832             =item on_stat => CODE
833              
834             Optional when not returning a future. Continuation to invoke on success. Is
835             passed a list of lines from the C result, one line per element.
836              
837             $on_stat->( @stat )
838              
839             =item on_error => CODE
840              
841             Optional. Continuation to invoke on an error.
842              
843             $on_error->( $message )
844              
845             =back
846              
847             The C method may be easier to use as it parses the lines.
848              
849             =cut
850              
851             sub stat
852             {
853 7     7 1 2848 my $self = shift;
854 7         22 my %args = @_;
855              
856 7         16 my $path = $args{path}; # optional
857              
858 7 50 66     45 my $on_stat = $args{on_stat} or defined wantarray or croak "Expected 'on_stat'";
859              
860 7         14 my $on_error = $args{on_error};
861 7 100 50 0   30 $on_error ||= sub { die "Error $_[0] during STAT" } if !defined wantarray;
  0         0  
862              
863             my $f = $self->do_command( defined $path ? "STAT $path" : "STAT", [ 211 ] )
864             ->then( sub {
865 7     7   443 my ( $num, $message, $headline, @statlines ) = @_;
866 7         36 return Future->new->done( @statlines );
867 7 50       54 });
868              
869 7 100       482 $f->on_done( $on_stat ) if $on_stat;
870 7 100       169 $f->on_fail( $on_error ) if $on_error;
871              
872 7 100       67 return $f if defined wantarray;
873 2     2   14 $f->on_ready( sub { undef $f } ); # Intentional cycle
  2         203  
874             }
875              
876             =head2 $ftp->stat_parsed( %args ) ==> @stat
877              
878             Runs a C command on a path on the remote server; and parse the result
879             lines. Takes the following arguments
880              
881             =over 8
882              
883             =item path => STRING
884              
885             Path to C
886              
887             =item on_stat => CODE
888              
889             Optional when returning a future. Continuation to invoke on success. Is passed
890             a list of lines from the C result, one line per element.
891              
892             $on_stat->( @stat )
893              
894             =item on_error => CODE
895              
896             Optional. Continuation to invoke on an error.
897              
898             $on_error->( $message )
899              
900             =back
901              
902             The C<@stat> array will be passed a list of C references, each formed
903             like
904              
905             =over 8
906              
907             =item name => STRING
908              
909             The filename
910              
911             =item type => STRING
912              
913             A single character; C for files, C for directories
914              
915             =item size => INT
916              
917             The size in bytes
918              
919             =item mtime => INT
920              
921             The item's last modify timestamp, as a UNIX epoch time
922              
923             =item mode => INT
924              
925             The access mode, as a number
926              
927             =back
928              
929             If C is invoked on a file, then C<@stat> will contain a single reference
930             to represent it. If invoked on a directory, the C<@stat> will start with a
931             reference about the directory itself (whose name will be C<.>), then one per
932             item in the directory, in the order the server returned the lines.
933              
934             =cut
935              
936             sub stat_parsed
937             {
938 4     4 1 12711 my $self = shift;
939 4         17 my %args = @_;
940              
941 4 50       16 defined $args{path} or croak "Expected 'path'";
942              
943 4 50 66     22 my $on_stat = $args{on_stat} or defined wantarray or croak "Expected 'on_stat'";
944              
945 4         838 require File::Listing;
946              
947 4         12233 my $on_error = $args{on_error};
948 4 100 50 0   29 $on_error ||= sub { die "Error $_[0] during stat_parsed" } if !defined wantarray;
  0         0  
949              
950             my $f = $self->stat(
951             path => $args{path},
952             )->then( sub {
953 4     4   463 my @statlines = @_;
954              
955 4         9 my @pstats;
956              
957 4 100       12 if( @statlines > 1 ) {
958             # path is a directory. In that case, look for the . item
959             # This would be easy only File::Listing::parse_dir WILL
960             # ignore it and we don't get a say in the matter.
961             # In this case, we'll do a bit of cheating. We'll look for the
962             # "." line ourselves, mangle its name to "DIR", and mangle it
963             # back on the other end.
964              
965 2         4 my @lines_with_cwd;
966             my @lines_without_cwd;
967              
968 2         4 foreach ( @statlines ) {
969 6 100       26 m/ \.$/ ? ( push @lines_with_cwd, $_ ) : ( push @lines_without_cwd, $_ );
970             }
971              
972 2 50       7 @lines_with_cwd == 1 or
973             return $on_error->( "Did not find '.' in LIST output on directory $args{path}" );
974              
975 2         5 my $l = $lines_with_cwd[0];
976 2         8 $l =~ s/ \.$/ DIR/;
977              
978 2         12 ( my $cwdstat ) = File::Listing::parse_dir( $l );
979              
980 2 50       499 $cwdstat->[0] eq "DIR" or
981             return $on_error->( "Parsed listing did not contain DIR as the name like we expected for $args{path}" );
982              
983 2         5 $cwdstat->[0] = ".";
984              
985 2         9 @pstats = ( $cwdstat, File::Listing::parse_dir( \@lines_without_cwd ) );
986             }
987             else {
988 2         16 @pstats = File::Listing::parse_dir( $statlines[0] );
989             }
990              
991             # We want to present a HASH refs, as they're nicer to work with
992 4         1177 foreach ( @pstats ) {
993 8         12 my %h;
994 8         44 @h{qw( name type size mtime mode )} = @$_;
995 8         27 $_ = \%h;
996             }
997              
998 4         19 return Future->new->done( @pstats );
999 4         20 });
1000              
1001 4 100       224 $f->on_done( $on_stat ) if $on_stat;
1002 4 100       32 $f->on_fail( $on_error ) if $on_error;
1003              
1004 4 100       38 return $f if defined wantarray;
1005 2     2   13 $f->on_ready( sub { undef $f } ); # Intentional cycle
  2         242  
1006             }
1007              
1008             =head2 $ftp->stor( %args ) ==> ()
1009              
1010             Stores a file on the remote server. Takes the following arguments
1011              
1012             =over 8
1013              
1014             =item path => STRING
1015              
1016             Path to file to store
1017              
1018             =item data => STRING
1019              
1020             New contents for the file
1021              
1022             =item on_stored => CODE
1023              
1024             Optional when returning a future. Continuation to invoke on success.
1025              
1026             $on_stored->()
1027              
1028             =item on_error => CODE
1029              
1030             Optional. Continuation to invoke on an error.
1031              
1032             $on_error->( $message )
1033              
1034             =back
1035              
1036             =cut
1037              
1038             sub stor
1039             {
1040 2     2 1 856 my $self = shift;
1041 2         11 my %args = @_;
1042              
1043 2         6 my $path = $args{path};
1044 2 50       9 defined $path or croak "Expected 'path'";
1045              
1046 2         5 my $data = $args{data};
1047 2 50       7 defined $data or croak "Expected 'data'";
1048              
1049 2 50 66     23 my $on_stored = $args{on_stored} or defined wantarray or croak "Expected 'on_stored'";
1050              
1051 2         5 my $on_error = $args{on_error};
1052 2 100 50 0   15 $on_error ||= sub { die "Error $_[0] during STOR" } if !defined wantarray;
  0         0  
1053              
1054 2         12 my $f = $self->_do_command_send_dataconn(
1055             "STOR $path",
1056             $data,
1057             );
1058              
1059 2 100       11 $f->on_done( $on_stored ) if $on_stored;
1060 2 100       18 $f->on_fail( $on_error ) if $on_error;
1061              
1062 2         14 return $f;
1063             }
1064              
1065             =head1 SEE ALSO
1066              
1067             =over 4
1068              
1069             =item *
1070              
1071             L - FILE TRANSFER PROTOCOL (FTP)
1072              
1073             =back
1074              
1075             =head1 AUTHOR
1076              
1077             Paul Evans
1078              
1079             =cut
1080              
1081             0x55AA;