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   3423 use 5.010;
  24         101  
4 24     24   718 use Moo;
  24         12671  
  24         184  
5 24     24   12972 use AnyEvent;
  24         6067  
  24         855  
6 24     24   865 use AnyEvent::Socket qw( tcp_connect );
  24         28685  
  24         1456  
7 24     24   1049 use AnyEvent::Handle;
  24         8629  
  24         825  
8 24     24   148 use Carp qw( croak );
  24         48  
  24         1331  
9 24     24   141 use Socket qw( unpack_sockaddr_in inet_ntoa );
  24         74  
  24         50336  
10              
11             # ABSTRACT: Simple asynchronous ftp client
12             our $VERSION = '0.18'; # 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 414 my($self) = @_;
53 63     0   633 $self->on_error(sub { warn shift });
  0         0  
54             $self->on_close(sub {
55 41     41   242 $self->clear_command;
56 41         148 $self->_connected(0);
57 41         157 delete $self->{handle};
58 63         394 });
59              
60 63 100       13589 require ($self->passive
61             ? 'AnyEvent/FTP/Client/Transfer/Passive.pm'
62             : 'AnyEvent/FTP/Client/Transfer/Active.pm');
63              
64 63         385 return;
65             }
66              
67              
68             sub connect
69             {
70 69     69 1 19927 my($self, $host, $port) = @_;
71              
72 69 100       409 if($host =~ /^ftp:/)
73             {
74 31         423 require URI;
75 31         212 $host = URI->new($host);
76             }
77              
78 69         3566 my $uri;
79              
80 69 100 66     383 if(ref($host) && eval { $host->isa('URI') })
  31         439  
81             {
82 31         79 $uri = $host;
83 31         178 $host = $uri->host;
84 31         1420 $port = $uri->port;
85             }
86             else
87             {
88 38   50     111 $port //= 21;
89             }
90              
91 69 50       1208 croak "Tried to reconnect while connected" if $self->_connected;
92              
93 69         1775 my $cv = AnyEvent->condvar;
94 69         989 $self->_connected(1);
95              
96             tcp_connect $host, $port, sub {
97 69     69   8183 my($fh) = @_;
98 69 50       271 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         167 $self->{my_ip} = do {
109 69         839 my($port, $addr) = unpack_sockaddr_in getsockname $fh;
110 69         792 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         3510 $self->{handle}->destroy;
123 41         3845 $self->emit('close');
124             },
125 69         1132 );
126              
127             $self->on_next_response(sub {
128 69         145 my $res = shift;
129 69 50       320 return $cv->croak($res) unless $res->is_success;
130 69         396 $self->emit(greeting => $res);
131 69 100       245 if(defined $uri)
132             {
133 31         211 my @start_commands = (
134             [USER => $uri->user],
135             [PASS => $uri->password],
136             );
137 31 100       3976 push @start_commands, [CWD => $uri->path] if $uri->path ne '';
138 31         859 $self->unshift_command(@start_commands, $cv);
139             }
140             else
141             {
142 38         179 $cv->send($res);
143 38         575 $self->pop_command;
144             }
145 69         9108 });
146              
147             $self->{handle}->on_read(sub {
148             $self->{handle}->push_read( line => sub {
149 825         45686 my($handle, $line) = @_;
150 825         3418 $self->process_message_line($line);
151 825         806512 });
152 69         552 });
153              
154             }, sub {
155 69     69   29257 $self->timeout;
156 69         973 };
157              
158 69         12398 return $cv;
159             }
160              
161              
162             sub login
163             {
164 32     32 1 151 my($self, $user, $pass) = @_;
165 32         203 $self->push_command(
166             [ USER => $user ],
167             [ PASS => $pass ]
168             );
169             }
170              
171              
172             sub retr
173             {
174 21     21 1 27013 my($self, $filename, $local) = (shift, shift, shift);
175 21 50       161 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         862 });
182             }
183              
184              
185             sub stor
186             {
187 15     15 1 26003 my($self, $filename, $local) = @_;
188 15         624 $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 12 my($self, $filename, $local) = @_;
199 3         8 my $xfer;
200             my $cb = sub {
201 6     6   32 my $name = shift->get_file;
202 6 100       32 $xfer->{remote_name} = $name if defined $name;
203 6         23 return;
204 3         26 };
205 3         96 $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 1161 my($self, $filename, $local) = @_;
217 7         264 $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 10387 my($self, $location, $verb) = @_;
228 24   100     213 $verb //= 'LIST';
229 24         86 my @lines;
230 24         942 my $cv = AnyEvent->condvar;
231             $self->_list->new(
232             command => [ $verb => $location ],
233             local => \@lines,
234             client => $self,
235             )->cb(sub {
236 24     24   305 my $res = eval { shift->recv };
  24         211  
237 24 50       112 $cv->croak($@) if $@;
238 24         159 $cv->send(\@lines);
239 24         739 });
240 24         375 $cv;
241             }
242              
243              
244             sub nlst
245             {
246 16     16 1 3036 my($self, $location) = @_;
247 16         90 $self->list($location, 'NLST');
248             }
249              
250              
251             sub rename
252             {
253 3     3 1 3044 my($self, $from, $to) = @_;
254 3         62 $self->push_command(
255             [ RNFR => $from ],
256             [ RNTO => $to ],
257             );
258             }
259              
260              
261             sub pwd
262             {
263 7     7 1 681 my($self) = @_;
264 7         240 my $cv = AnyEvent->condvar;
265             $self->push_command(['PWD'])->cb(sub {
266 7   33 7   70 my $res = eval { shift->recv } // $@;
  7         29  
267 7         40 my $dir = $res->get_dir;
268 7 50       24 if($dir) { $cv->send($dir) }
  7         32  
269 0         0 else { $cv->croak($res) }
270 7         103 });
271 7         102 $cv;
272             }
273              
274              
275             sub size
276             {
277 6     6 1 5440 my($self, $path) = @_;
278 6         254 my $cv = AnyEvent->condvar;
279             $self->push_command(['SIZE', $path])->cb(sub {
280 6     6   61 my $res = eval { shift->recv };
  6         27  
281 6 50       28 if(my $error = $@)
282 0         0 { $cv->croak($error) }
283             else
284 6         25 { $cv->send($res->message->[0]) }
285 6         102 });
286 6         102 $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 96  
  3     3 1 760  
  27     27 1 1052  
  4     4 1 2241  
  106     106 1 2065  
  0     0 1 0  
  3     3 1 1271  
  0     0 1 0  
  3     3 1 38  
  0     0 1 0  
  2     2 1 22  
  3     3 1 48  
  1     1 1 24  
  1     1 1 661  
  3     3 1 1271  
  0     0 1 0  
  3     3 1 18  
  26     26 1 224  
  0     0 1 0  
292              
293              
294             sub quit
295             {
296 38     38 1 33883 my($self) = @_;
297 38         1571 my $cv = AnyEvent->condvar;
298              
299 38         348 my $res;
300              
301             $self->push_command(['QUIT'])->cb(sub {
302 38   33 38   471 $res = eval { shift->recv } // $@;
  38         149  
303 38         256 });
304              
305 38         518 my $save = $self->{event}->{close};
306             $self->{event}->{close} = [ sub {
307 38 50 33 38   282 if(defined $res && $res->is_success)
    0          
308 38         148 { $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         561 $_->() for @$save;
314 38         119 $self->{event}->{close} = $save;
315 38         217 } ];
316              
317 38         232 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__