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   3651 use 5.010;
  24         226  
4 24     24   758 use Moo;
  24         9957  
  24         175  
5 24     24   14683 use AnyEvent;
  24         7577  
  24         946  
6 24     24   769 use AnyEvent::Socket qw( tcp_connect );
  24         35350  
  24         1567  
7 24     24   1039 use AnyEvent::Handle;
  24         10799  
  24         703  
8 24     24   118 use Carp qw( croak );
  24         43  
  24         1558  
9 24     24   171 use Socket qw( unpack_sockaddr_in inet_ntoa );
  24         46  
  24         54934  
10              
11             # ABSTRACT: Simple asynchronous ftp client
12             our $VERSION = '0.20'; # 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 446 my($self) = @_;
53 63     0   558 $self->on_error(sub { warn shift });
  0         0  
54             $self->on_close(sub {
55 41     41   250 $self->clear_command;
56 41         141 $self->_connected(0);
57 41         155 delete $self->{handle};
58 63         532 });
59              
60 63 100       14543 require ($self->passive
61             ? 'AnyEvent/FTP/Client/Transfer/Passive.pm'
62             : 'AnyEvent/FTP/Client/Transfer/Active.pm');
63              
64 63         496 return;
65             }
66              
67              
68             sub connect
69             {
70 69     69 1 15015 my($self, $host, $port) = @_;
71              
72 69 100       399 if($host =~ /^ftp:/)
73             {
74 31         443 require URI;
75 31         209 $host = URI->new($host);
76             }
77              
78 69         3935 my $uri;
79              
80 69 100 66     413 if(ref($host) && eval { $host->isa('URI') })
  31         492  
81             {
82 31         76 $uri = $host;
83 31         194 $host = $uri->host;
84 31         1444 $port = $uri->port;
85             }
86             else
87             {
88 38   50     151 $port //= 21;
89             }
90              
91 69 50       1283 croak "Tried to reconnect while connected" if $self->_connected;
92              
93 69         2153 my $cv = AnyEvent->condvar;
94 69         1158 $self->_connected(1);
95              
96             tcp_connect $host, $port, sub {
97 69     69   7113 my($fh) = @_;
98 69 50       350 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         125 $self->{my_ip} = do {
109 69         587 my($port, $addr) = unpack_sockaddr_in getsockname $fh;
110 69         594 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         5207 $self->{handle}->destroy;
123 41         1594 $self->emit('close');
124             },
125 69         1149 );
126              
127             $self->on_next_response(sub {
128 69         138 my $res = shift;
129 69 50       333 return $cv->croak($res) unless $res->is_success;
130 69         366 $self->emit(greeting => $res);
131 69 100       226 if(defined $uri)
132             {
133 31         195 my @start_commands = (
134             [USER => $uri->user],
135             [PASS => $uri->password],
136             );
137 31 100       4181 push @start_commands, [CWD => $uri->path] if $uri->path ne '';
138 31         815 $self->unshift_command(@start_commands, $cv);
139             }
140             else
141             {
142 38         166 $cv->send($res);
143 38         662 $self->pop_command;
144             }
145 69         8012 });
146              
147             $self->{handle}->on_read(sub {
148             $self->{handle}->push_read( line => sub {
149 825         50366 my($handle, $line) = @_;
150 825         3890 $self->process_message_line($line);
151 825         2680636 });
152 69         809 });
153              
154             }, sub {
155 69     69   28942 $self->timeout;
156 69         1096 };
157              
158 69         13073 return $cv;
159             }
160              
161              
162             sub login
163             {
164 32     32 1 158 my($self, $user, $pass) = @_;
165 32         183 $self->push_command(
166             [ USER => $user ],
167             [ PASS => $pass ]
168             );
169             }
170              
171              
172             sub retr
173             {
174 21     21 1 28237 my($self, $filename, $local) = (shift, shift, shift);
175 21 50       133 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         1064 });
182             }
183              
184              
185             sub stor
186             {
187 15     15 1 39540 my($self, $filename, $local) = @_;
188 15         762 $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 10 my($self, $filename, $local) = @_;
199 3         8 my $xfer;
200             my $cb = sub {
201 6     6   37 my $name = shift->get_file;
202 6 100       23 $xfer->{remote_name} = $name if defined $name;
203 6         16 return;
204 3         34 };
205 3         92 $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 1313 my($self, $filename, $local) = @_;
217 7         260 $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 10577 my($self, $location, $verb) = @_;
228 24   100     180 $verb //= 'LIST';
229 24         74 my @lines;
230 24         1220 my $cv = AnyEvent->condvar;
231             $self->_list->new(
232             command => [ $verb => $location ],
233             local => \@lines,
234             client => $self,
235             )->cb(sub {
236 24     24   290 my $res = eval { shift->recv };
  24         101  
237 24 50       123 $cv->croak($@) if $@;
238 24         166 $cv->send(\@lines);
239 24         843 });
240 24         372 $cv;
241             }
242              
243              
244             sub nlst
245             {
246 16     16 1 5297 my($self, $location) = @_;
247 16         85 $self->list($location, 'NLST');
248             }
249              
250              
251             sub rename
252             {
253 3     3 1 6723 my($self, $from, $to) = @_;
254 3         55 $self->push_command(
255             [ RNFR => $from ],
256             [ RNTO => $to ],
257             );
258             }
259              
260              
261             sub pwd
262             {
263 7     7 1 719 my($self) = @_;
264 7         260 my $cv = AnyEvent->condvar;
265             $self->push_command(['PWD'])->cb(sub {
266 7   33 7   57 my $res = eval { shift->recv } // $@;
  7         22  
267 7         54 my $dir = $res->get_dir;
268 7 50       18 if($dir) { $cv->send($dir) }
  7         24  
269 0         0 else { $cv->croak($res) }
270 7         77 });
271 7         76 $cv;
272             }
273              
274              
275             sub size
276             {
277 6     6 1 6240 my($self, $path) = @_;
278 6         293 my $cv = AnyEvent->condvar;
279             $self->push_command(['SIZE', $path])->cb(sub {
280 6     6   58 my $res = eval { shift->recv };
  6         19  
281 6 50       21 if(my $error = $@)
282 0         0 { $cv->croak($error) }
283             else
284 6         35 { $cv->send($res->message->[0]) }
285 6         124 });
286 6         79 $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 128  
  3     3 1 436  
  27     27 1 735  
  4     4 1 3247  
  106     106 1 2180  
  0     0 1 0  
  3     3 1 1693  
  0     0 1 0  
  3     3 1 31  
  0     0 1 0  
  2     2 1 14  
  3     3 1 33  
  1     1 1 15  
  1     1 1 553  
  3     3 1 1050  
  0     0 1 0  
  3     3 1 20  
  26     26 1 192  
  0     0 1 0  
292              
293              
294             sub quit
295             {
296 38     38 1 40929 my($self) = @_;
297 38         1647 my $cv = AnyEvent->condvar;
298              
299 38         328 my $res;
300              
301             $self->push_command(['QUIT'])->cb(sub {
302 38   33 38   403 $res = eval { shift->recv } // $@;
  38         122  
303 38         225 });
304              
305 38         450 my $save = $self->{event}->{close};
306             $self->{event}->{close} = [ sub {
307 38 50 33 38   213 if(defined $res && $res->is_success)
    0          
308 38         130 { $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         474 $_->() for @$save;
314 38         106 $self->{event}->{close} = $save;
315 38         225 } ];
316              
317 38         189 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__