File Coverage

blib/lib/AnyEvent/FTP/Client.pm
Criterion Covered Total %
statement 141 163 86.5
branch 20 30 66.6
condition 8 16 50.0
subroutine 43 51 84.3
pod 33 34 97.0
total 245 294 83.3


line stmt bran cond sub pod time code
1             package AnyEvent::FTP::Client;
2              
3 24     24   2444 use 5.010;
  24         71  
4 24     24   480 use Moo;
  24         8211  
  24         146  
5 24     24   10447 use AnyEvent;
  24         3991  
  24         575  
6 24     24   540 use AnyEvent::Socket qw( tcp_connect );
  24         18687  
  24         1128  
7 24     24   699 use AnyEvent::Handle;
  24         5882  
  24         498  
8 24     24   120 use Carp qw( croak );
  24         43  
  24         1032  
9 24     24   115 use Socket qw( unpack_sockaddr_in inet_ntoa );
  24         40  
  24         35451  
10              
11             # ABSTRACT: Simple asynchronous ftp client
12             our $VERSION = '0.19'; # VERSION
13              
14              
15             with 'AnyEvent::FTP::Role::Event';
16             with 'AnyEvent::FTP::Client::Role::ResponseBuffer';
17             with 'AnyEvent::FTP::Client::Role::RequestBuffer';
18              
19              
20             __PACKAGE__->define_events(qw( error close send greeting ));
21              
22             has _connected => (
23             is => 'rw',
24             default => sub { 0 },
25             init_arg => undef,
26             );
27              
28              
29             has timeout => (
30             is => 'rw',
31             default => sub { 30 },
32             );
33              
34              
35             has passive => (
36             is => 'ro',
37             default => sub { 1 },
38             );
39              
40             foreach my $xfer (qw( Store Fetch List ))
41             {
42             my $cb = sub {
43             return shift->passive
44             ? 'AnyEvent::FTP::Client::Transfer::Passive::'.$xfer
45             : 'AnyEvent::FTP::Client::Transfer::Active::'.$xfer;
46             };
47             has '_'.lc($xfer) => ( is => 'ro', lazy => 1, default => $cb, init_arg => undef ),
48             }
49              
50             sub BUILD
51             {
52 63     63 0 322 my($self) = @_;
53 63     0   469 $self->on_error(sub { warn shift });
  0         0  
54             $self->on_close(sub {
55 41     41   183 $self->clear_command;
56 41         131 $self->_connected(0);
57 41         113 delete $self->{handle};
58 63         405 });
59              
60 63 100       20247 require ($self->passive
61             ? 'AnyEvent/FTP/Client/Transfer/Passive.pm'
62             : 'AnyEvent/FTP/Client/Transfer/Active.pm');
63              
64 63         342 return;
65             }
66              
67              
68             sub connect
69             {
70 69     69 1 13380 my($self, $host, $port) = @_;
71              
72 69 100       332 if($host =~ /^ftp:/)
73             {
74 31         347 require URI;
75 31         187 $host = URI->new($host);
76             }
77              
78 69         2981 my $uri;
79              
80 69 100 66     302 if(ref($host) && eval { $host->isa('URI') })
  31         383  
81             {
82 31         74 $uri = $host;
83 31         176 $host = $uri->host;
84 31         1296 $port = $uri->port;
85             }
86             else
87             {
88 38   50     82 $port //= 21;
89             }
90              
91 69 50       985 croak "Tried to reconnect while connected" if $self->_connected;
92              
93 69         1318 my $cv = AnyEvent->condvar;
94 69         732 $self->_connected(1);
95              
96             tcp_connect $host, $port, sub {
97 69     69   6226 my($fh) = @_;
98 69 50       235 unless($fh)
99             {
100 0         0 $cv->croak("unable to connect: $!");
101 0         0 $self->_connected(0);
102 0         0 $self->clear_command;
103 0         0 return;
104             }
105              
106             # Get the IP address we are sending from for when
107             # we use the PORT command (passive=0).
108 69         116 $self->{my_ip} = do {
109 69         612 my($port, $addr) = unpack_sockaddr_in getsockname $fh;
110 69         569 inet_ntoa $addr;
111             };
112              
113             $self->{handle} = AnyEvent::Handle->new(
114             fh => $fh,
115             on_error => sub {
116 0         0 my ($hdl, $fatal, $msg) = @_;
117 0         0 $_[0]->destroy;
118 0         0 $self->emit('error', $msg);
119 0         0 $self->emit('close');
120             },
121             on_eof => sub {
122 41         2470 $self->{handle}->destroy;
123 41         1199 $self->emit('close');
124             },
125 69         816 );
126              
127             $self->on_next_response(sub {
128 69         116 my $res = shift;
129 69 50       265 return $cv->croak($res) unless $res->is_success;
130 69         299 $self->emit(greeting => $res);
131 69 100       191 if(defined $uri)
132             {
133 31         162 my @start_commands = (
134             [USER => $uri->user],
135             [PASS => $uri->password],
136             );
137 31 100       3176 push @start_commands, [CWD => $uri->path] if $uri->path ne '';
138 31         726 $self->unshift_command(@start_commands, $cv);
139             }
140             else
141             {
142 38         178 $cv->send($res);
143 38         473 $self->pop_command;
144             }
145 69         7346 });
146              
147             $self->{handle}->on_read(sub {
148             $self->{handle}->push_read( line => sub {
149 825         31812 my($handle, $line) = @_;
150 825         2488 $self->process_message_line($line);
151 825         666527 });
152 69         408 });
153              
154             }, sub {
155 69     69   22895 $self->timeout;
156 69         857 };
157              
158 69         13704 return $cv;
159             }
160              
161              
162             sub login
163             {
164 32     32 1 119 my($self, $user, $pass) = @_;
165 32         156 $self->push_command(
166             [ USER => $user ],
167             [ PASS => $pass ]
168             );
169             }
170              
171              
172             sub retr
173             {
174 21     21 1 17846 my($self, $filename, $local) = (shift, shift, shift);
175 21 50       383 my $args = ref $_[0] eq 'HASH' ? (\%{$_[0]}) : ({@_});
  0         0  
176             $self->_fetch->new({
177             command => [ RETR => $filename ],
178             local => $local,
179             client => $self,
180             restart => $args->{restart},
181 21         485 });
182             }
183              
184              
185             sub stor
186             {
187 15     15 1 16654 my($self, $filename, $local) = @_;
188 15         342 $self->_store->new(
189             command => [STOR => $filename],
190             local => $local,
191             client => $self,
192             );
193             }
194              
195              
196             sub stou
197             {
198 3     3 1 9 my($self, $filename, $local) = @_;
199 3         5 my $xfer;
200             my $cb = sub {
201 6     6   90 my $name = shift->get_file;
202 6 100       28 $xfer->{remote_name} = $name if defined $name;
203 6         15 return;
204 3         17 };
205 3         64 $xfer = $self->_store->new(
206             command => [STOU => $filename, $cb],
207             local => $local,
208             client => $self,
209             );
210             }
211              
212              
213             # for this to work under ProFTPd: AllowStoreRestart off
214             sub appe
215             {
216 7     7 1 821 my($self, $filename, $local) = @_;
217 7         187 $self->_store->new(
218             command => [APPE => $filename],
219             local => $local,
220             client => $self,
221             );
222             }
223              
224              
225             sub list
226             {
227 24     24 1 6074 my($self, $location, $verb) = @_;
228 24   100     150 $verb //= 'LIST';
229 24         32 my @lines;
230 24         564 my $cv = AnyEvent->condvar;
231             $self->_list->new(
232             command => [ $verb => $location ],
233             local => \@lines,
234             client => $self,
235             )->cb(sub {
236 24     24   193 my $res = eval { shift->recv };
  24         123  
237 24 50       87 $cv->croak($@) if $@;
238 24         106 $cv->send(\@lines);
239 24         503 });
240 24         238 $cv;
241             }
242              
243              
244             sub nlst
245             {
246 16     16 1 2586 my($self, $location) = @_;
247 16         55 $self->list($location, 'NLST');
248             }
249              
250              
251             sub rename
252             {
253 3     3 1 1953 my($self, $from, $to) = @_;
254 3         32 $self->push_command(
255             [ RNFR => $from ],
256             [ RNTO => $to ],
257             );
258             }
259              
260              
261             sub pwd
262             {
263 7     7 1 557 my($self) = @_;
264 7         172 my $cv = AnyEvent->condvar;
265             $self->push_command(['PWD'])->cb(sub {
266 7   33 7   91 my $res = eval { shift->recv } // $@;
  7         24  
267 7         28 my $dir = $res->get_dir;
268 7 50       20 if($dir) { $cv->send($dir) }
  7         22  
269 0         0 else { $cv->croak($res) }
270 7         67 });
271 7         92 $cv;
272             }
273              
274              
275             sub size
276             {
277 6     6 1 4444 my($self, $path) = @_;
278 6         258 my $cv = AnyEvent->condvar;
279             $self->push_command(['SIZE', $path])->cb(sub {
280 6     6   51 my $res = eval { shift->recv };
  6         26  
281 6 50       24 if(my $error = $@)
282 0         0 { $cv->croak($error) }
283             else
284 6         25 { $cv->send($res->message->[0]) }
285 6         100 });
286 6         74 $cv;
287             }
288              
289              
290             (eval sprintf('sub %s { shift->push_command([ %s => @_])};1', lc $_, $_)) // die $@
291 0     0 1 0 for qw( CWD CDUP NOOP ALLO SYST TYPE STRU MODE REST MKD RMD STAT HELP DELE RNFR RNTO USER PASS ACCT MDTM );
  4     4 1 71  
  3     3 1 510  
  27     27 1 686  
  4     4 1 1621  
  106     106 1 1539  
  0     0 1 0  
  3     3 1 914  
  0     0 1 0  
  3     3 1 15  
  0     0 1 0  
  2     2 1 18  
  3     3 1 29  
  1     1 1 14  
  1     1 1 386  
  3     3 1 790  
  0     0 1 0  
  3     3 1 13  
  26     26 1 184  
  0     0 1 0  
292              
293              
294             sub quit
295             {
296 38     38 1 26278 my($self) = @_;
297 38         1082 my $cv = AnyEvent->condvar;
298              
299 38         282 my $res;
300              
301             $self->push_command(['QUIT'])->cb(sub {
302 38   33 38   267 $res = eval { shift->recv } // $@;
  38         85  
303 38         226 });
304              
305 38         373 my $save = $self->{event}->{close};
306             $self->{event}->{close} = [ sub {
307 38 50 33 38   1875 if(defined $res && $res->is_success)
    0          
308 38         111 { $cv->send($res) }
309             elsif(defined $res)
310 0         0 { $cv->croak($res) }
311             else
312 0         0 { $cv->croak("did not receive QUIT response from server") }
313 38         376 $_->() for @$save;
314 38         85 $self->{event}->{close} = $save;
315 38         212 } ];
316              
317 38         200 return $cv;
318             }
319              
320              
321             sub site
322             {
323 0     0 1   require AnyEvent::FTP::Client::Site;
324 0           AnyEvent::FTP::Client::Site->new(shift);
325             }
326              
327             1;
328              
329             __END__