AnyEvent::FTP::Client - Simple asynchronous ftp client
version 0.19
Non blocking example:
use strict; use warnings; use AnyEvent; use AnyEvent::FTP::Client; my $client = AnyEvent::FTP::Client->new( passive => 1); my $done = AnyEvent->condvar; # connect to CPAN ftp server $client->connect('ftp://ftp.cpan.org/pub/CPAN/src')->cb(sub { # use binary mode $client->type('I')->cb(sub { # download the file directly into a filehandle open my $fh, '>', 'perl-5.16.3.tar.gz'; $client->retr('perl-5.16.3.tar.gz', $fh)->cb(sub { # notify anyone listening to $done that # the transfer is complete $done->send; }); }); }); # receive the done message once the transfer is # complete. In real code you'd probably not # want to do this because your event loop may # not support blocking. $done->recv;
Same, but using recv to wait for each command to complete (not supported in all event loops):
use strict; use warnings; use AnyEvent; use AnyEvent::FTP::Client; my $client = AnyEvent::FTP::Client->new( passive => 1); my $done = AnyEvent->condvar; # connect to CPAN ftp server $client->connect('ftp://ftp.cpan.org/pub/CPAN/src')->recv; # use binary mode $client->type('I')->recv; # download the file directly into a filehandle open my $fh, '>', 'perl-5.16.3.tar.gz'; $client->retr('perl-5.16.3.tar.gz', $fh)->recv;
This class provides an AnyEvent client interface to the File Transfer Protocol (FTP).
This class consumes these roles:
For details on the event interface see AnyEvent::FTP::Role::Event.
This event gets fired on every command sent to the remote server. Keep in mind that some methods of AnyEvent::FTP may make multiple FTP commands in order to implement their functionality (for example, recv
, stor
, etc). One use of this event is to print out commands as they are sent for debugging:
$client->on_send(sub { my($cmd, $arguments) = @_; $arguments //= ''; # hide passwords $arguments = 'XXXX' if $cmd =~ /^pass$/i; say "CLIENT: $cmd $arguments"; });
This event is emitted when there is a network error with the remote server. It passes in a string which describes in human readable description of what went wrong.
$client->on_error(sub { my($message) = @_; warn "network error: $message"; });
This event is emitted when the connection with the remote server is closed, either due to an error, or when you send the FTP QUIT
command using the quid
method.
$client->on_close(sub { # called when connection closed });
This event gets fired on the first response returned from the server. This is usually a 220
message which may or may not reveal the server software.
$client->on_greeting(sub { # $res is a AnyEvent::FTP::Client::Response my($res) = @_; if($res->message->[0] =~ /ProFTPD/) { # detected a ProFTPD server } });
This event gets fired for each response returned from the server. This can be useful for printing the responses for debugging.
$client->on_each_response(sub { # $res isa AnyEvent::FTP::Client::Response my($res) = @_; print "SERVER: $res\n"; });
Works just like each_response
event, but only gets fired for the next response received.
Timeout for the initial connection to the FTP server. The default is 30.
If set to true (the default) then data will be transferred using the passive (PASV) command, meaning the server will open a port for the client to connect to. If set to false then data will be transferred using data port (PORT) command, meaning the client will open a port for the server to send to.
Unless otherwise specified, these methods will return an AnyEvent condition variable (AnyEvent->condvar) or an object that implements its interface (methods recv
, cb
). On success the send
will be used on the condition variable, on failure croak
will be used instead. Unless otherwise specified the object sent (for both success and failure) will be an instance of AnyEvent::FTP::Client::Response.
As an example, here is a fairly thorough handling of a response to the standard FTP HELP
command:
$client->help->cb(sub { my $res = eval { shift->recv }; if(my $error = $@) { # $error isa AnyEvent::FTP::Client::Response with a 4xx or 5xx # code my $code = $error->code; # the message component is always a list ref, even if # the response had just one message line my @msg = @{ $error->message }; # $error is stringified into something human readable when # it is streated as a string warn "error trying FTP HELP command: $error"; } else { # $res isa AnyEvent::FTP::Client::Response with a 2xx or 3xx # code my $code = $res->code; # the message component is always a list ref, even if # the response had just one message line my @msg = @{ $res->message }; # $res is stringified into something human readable when # it is streated as a string print "help message: $res"; } });
$client->connect(@remote_host);
Connect to the FTP server. The remote host may be specified in one of these ways:
The host and port of the remote server. If not specified, the default FTP port will be used (21).
The URI of the remote FTP server. $uri
must be either an instance of URI with the ftp
scheme, or a string with an FTP URL.
If you use this method to connect to the FTP server, connect will also attempt to login with the username and password specified in the URL (or anonymous FTP if no credentials are specified).
If there is a path included in the URL, then connect will also do a CWD
so that you start in that directory.
$client->login($user, $pass);
Attempt to login to the FTP server which has already been connected to using the connect
method. This is not necessary if you used connect
with a URI.
$client->retr($filename, $local, %options)
Retrieve the given file from the server and use $local
to store the results.
Returns an instance of AnyEvent::FTP::Client::Transfer, which supports the AnyEvent condition variable interface (that is it has cb
and recv
methods). Its callback will be called when the transfer is complete.
$local
may be one of
The contents of the file will be stored in the scalar referred to by the reference.
my $local; $client->retr('foo.txt', \$local);
The content of the remote file will be written into the local file handle as it is received
open my $fh, '>', 'foo.txt'; binmode $fh; $client->retr('foo.txt', $fh);
If $local
is just a regular non reference scalar, then it will be treated as the local filename, which will be created and written to as data is received from the server.
$client->retr('foo.txt', 'foo.txt');
The contents of the file will be passed to the callback as they are received.
$client->retr('foo.txt', sub { my ($data) = @_; # Do something with $data }, );
In order to resume a transfer, you need to include the restart
option after the $local
argument. Here is an example:
# assumes foo.txt (partial download) exists in the current # loacal directory and foo.txt (full file) exists in the # current remote directory. my $filename = 'foo.txt'; open my $fh, '>>', $filename; binmode $fh; $client->retr($filename, $fh, restart => tell $fh);
$client->stor($filename, $local);
Send a file to the server with the given remote filename ($filename
) and using $local
as a source.
Returns an instance of AnyEvent::FTP::Client::Transfer, which supports the AnyEvent condition variable interface (that is it has cb
and recv
methods). Its callback will be called when the transfer is complete.
$local
may be one of
The contents of the file will be retrieved from the scalar referred to by the reference.
my $local = 'some data for foo.txt'; $client->stor('foo.txt', \$local);
The contents of the file will be read from the file handle.
open my $fh, '<', 'foo.txt'; binmode $fh; $client->stor('foo.txt', $fh);
If $local
is just a regular non reference scalar, then it will be treated as the local filename, which will be opened and read from in order to create the file on the server.
$client->stor('foo.txt', 'foo.txt');
$client->stou($filename, $local)
Works exactly like the stor
method, except use the FTP STOU
command instead of STOR
. Since the remote filename is optional for STOU
you may pass in undef
as the remote filename. You can get the remote filename after the fact using the remote_name
method.
my $xfer; $xfer = $client->stou(undef, $local)->cb(sub { my $remote_filename = $xfer->remote_name; });
$client->appe($filename, $local);
Works exactly like the stor
method, except use the FTP APPE
command instead of STOR
. This method will append $local
to the remote file. One way to resume an upload to the remote FTP server would be to open the local file, determine the remote file's size and seek to that position in the local file and use the appe
method with $local
as that file handle, as in this example:
# assume that foo.txt is in the current local dir # and the remote local dir my $filename = "foo.txt"; $client->size($filename)->cb(sub { my $size = shift->recv; open my $fh, '<', $filename; binmode $fh; seek $fh, $size, 0; $client->appe($filename, $fh); });
Note that the SIZE
command is an extension to FTP, and may not be available on all servers.
$client->list($location)
Execute the FTP LIST
command. The results will be sent as a list reference (instead of a AnyEvent::FTP::Client::Response object) to the returned condition variable.
use strict; use warnings; use AnyEvent; use AnyEvent::FTP::Client; my $client = AnyEvent::FTP::Client->new; my $cv = AnyEvent->condvar; # connect to CPAN ftp server $client->connect('ftp://ftp.cpan.org/pub/CPAN/src')->cb(sub { # execute LIST command and print results to stdout $client->list->cb(sub { my $list = shift->recv; print "$_\n" for @$list; $cv->send; }); }); $cv->recv;
$client->nlst($location);
Works exactly like the list
method, except the FTP NLST
command is used. The main difference is that this method returns filenames only.
$client->rename($from, $to);
This method renames the remote file from $from
to $to
. It uses the FTP RNFR
and RNTO
commands and thus this:
my $cv = $client->rename($from, $to);
is a short cut for:
my $cv; $client->rnfr($from)->cb(sub { $cv = $client->rnto($to); });
Although $cv
may not be defined right away, so use the second with care.
$client->cwd( $dir );
Change to the given directory on the remote server.
$client->pwd;
Gets the current working directory on the remote server. This gets just the string representing the directory path instead of a AnyEvent::FTP::Client::Response object.
$client->cdup
Change to the parent directory on the remote server. This is usually the same as
$client->cwd('..');
$client->type
Set the transfer type. You almost always want to set to binary mode immediately after logging on:
$client->type('I');
$client->rest
This command is used to resume a download transfer. Typically you would not use this method directly, but instead add a restart
option on the retr
method instead.
$client->mkd( $path );
Create a directory on the remote server.
$client->rmd( $path );
Remove a directory on the remote server.
$client->help;
Gets a list of commands understood by the server. The actual format depends on the server.
$client->dele( $path );
Delete the file on the remote server.
$client->rnfr;
Specify the old name for renaming a file. See rename
method for a shortcut.
$client->rnto;
Specify the new name for renaming a file. See rename
method for a shortcut.
$client->noop;
Don't do anything. The server will send an OK reply.
$client->allo( $size );
Send the FTP ALLO
command. Is not used by modern FTP servers. See RFC959 for details.
$client->syst;
Returns the type of operating system used by the server.
$client->stru;
Specify the file structure mode. This is not used by modern FTP servers. See RFC959 for details.
$client->mode
Specify the transfer mode. This is not used by modern FTP servers. See RFC959 for details.
$client->stat; $client->stat($path);
Get information about a file or directory on the remote server. The actual format is totally server dependent.
$client->user( $username );
Specify the user to login as. See connect
or login
methods for a shortcut.
$client->pass( $pass );
Specify the password to use for login. See connect
or login
methods for a shortcut.
$client->acct( $acct );
Specify user's account. This is sometimes used for authentication and authorization when you login to some servers, but is seldom used today in practice. See RFC959 for details.
$client->size( $path );
Get the size of the remote file specified by $path
. This is an extension to the FTP standard specified in RFC3659, and may not be implemented by older (or even newer) servers.
Send the size of the file on success, instead of the response object.
$client->mdtm( $path );
Get the modification time of the remote file specified by $path
. This is an extension to the FTP standard specified in RFC3659, and may not be implemented by older (or even newer) servers.
$client->quit;
Send the FTP QUIT
command and close the connection to the remote server.
$client->site;
The site
method provides an interface to site specific FTP commands. Many FTP servers will support an extended set of commands using the standard FTP SITE
command. This command will not check to see if the site commands are supported by the remote server, so it is up to you to determine if you can really use these interfaces yourself.
For commands specific to Microsoft's IIS FTP server. See AnyEvent::FTP::Client::Site::Microsoft.
For commands specific to Net::FTPServer. See AnyEvent::FTP::Client::Site::NetFtpServer.
For commands specific to proftpd. See AnyEvent::FTP::Client::Site::Proftpd.
Here are some longer examples. They are also included with the AnyEvent::FTP distribution in its example
directory.
Given a URL to a file, this script will fetch the file and store it on your local machine. If you use the -d
option you can see the FTP commands and their responses as they happen.
#!/usr/bin/perl use strict; use warnings; use autodie; use 5.010; use AnyEvent::FTP::Client; use URI; use URI::file; use Term::ProgressBar; use Term::Prompt qw( prompt ); use Getopt::Long qw( GetOptions ); use Path::Class qw( file ); my $debug = 0; my $progress = 0; my $active = 0; GetOptions( 'd' => \$debug, 'p' => \$progress, 'a' => \$active, ); my $remote = shift; unless(defined $remote) { say STDERR "usage: perl fget.pl [ -d | -p ] [ -a ] remote"; say STDERR " where remote is a URL for a file on an FTP server"; say STDERR " and local is a local filename (optional) where to transfer it to"; say STDERR " -d (optional) prints FTP commands and responses"; say STDERR " -p (optional) displays a progress bar as the file uploads"; say STDERR " -a (optional) use active mode transfer"; exit 2; } $remote = URI->new($remote); unless($remote->scheme eq 'ftp') { say STDERR "only FTP URLs are supported"; exit 2; } unless(defined $remote->password) { $remote->password(prompt('p', 'Password: ', '', '')); say ''; } do { my $from = $remote->clone; $from->password(undef); say "SRC: ", $from; }; my @path = split /\//, $remote->path; my $fn = pop @path; if(-e $fn) { say STDERR "local file already exists"; exit 2; } my $ftp = AnyEvent::FTP::Client->new( passive => $active ? 0 : 1 ); $ftp->on_send(sub { my($cmd, $arguments) = @_; $arguments //= ''; $arguments = 'XXXX' if $cmd eq 'PASS'; say "CLIENT: $cmd $arguments" if $debug; }); $ftp->on_each_response(sub { my $res = shift; if($debug) { say sprintf "SERVER: [ %d ] %s", $res->code, $_ for @{ $res->message }; } }); $ftp->connect($remote->host, $remote->port)->recv; $ftp->login($remote->user, $remote->password)->recv; $ftp->type('I')->recv; $ftp->cwd(join '/', '', @path)->recv; my $remote_size; if($progress) { my $listing = $ftp->list($fn)->recv; foreach my $class (qw( File::Listing File::Listing::Ftpcopy )) { my $parsed_listing = eval qq{ use $class; ${class}::parse_dir(\$listing->[0]) }; next if $@; my ($name, $type, $size, $mtime, $mode) = @{ $parsed_listing->[0] }; $remote_size = $size; last; } if(defined $remote_size) { } else { say STDERR "could not determine size of remote file, cannot provide progress bar"; $progress = 0; } } open my $fh, '>', $fn; my $xfer = $ftp->retr($fn); my $pb; my $count = 0; $xfer->on_open(sub { my $handle = shift; $pb = Term::ProgressBar->new({ count => $remote_size }) if $progress; $handle->on_read(sub { $handle->push_read(sub { print $fh $_[0]{rbuf}; $pb->update($count += length($_[0]{rbuf})) if $pb; $_[0]{rbuf} = ''; }); }); }); $xfer->recv; close $fh; $ftp->quit->recv;
Here is a similar example, which does a directory listing on a FTP directory URL. If you use the -d
option to see the FTP commands and their responses as they happen. You can use the -l
option to see the long form of the file listing.
use strict; use warnings; use 5.010; use URI; use AnyEvent::FTP::Client; use Term::Prompt qw( prompt ); use Getopt::Long qw( GetOptions ); my $debug = 0; my $method = 'nlst'; GetOptions( 'd' => \$debug, 'l' => sub { $method = 'list' }, ); my $ftp = AnyEvent::FTP::Client->new; if($debug) { $ftp->on_send(sub { my($cmd, $arguments) = @_; $arguments //= ''; $arguments = 'XXXX' if $cmd eq 'PASS'; say "CLIENT: $cmd $arguments"; }); $ftp->on_each_response(sub { my $res = shift; say sprintf "SERVER: [ %d ] %s", $res->code, $_ for @{ $res->message }; }); } my $uri = shift; unless(defined $uri) { say STDERR "usage: perl fls.pl URL\n"; exit 2; } $uri = URI->new($uri); unless($uri->scheme eq 'ftp') { say STDERR "only FTP URL accpeted"; exit 2; } unless(defined $uri->password) { $uri->password(prompt('p', 'Password: ', '', '')); say ''; } my $path = $uri->path; $uri->path(''); $ftp->connect($uri); say $_ for @{ $ftp->$method($path)->recv };
This script uploads a local file to the remote given a local filename and a remote FTP URL.
#!/usr/bin/perl use strict; use warnings; use autodie; use 5.010; use AnyEvent::FTP::Client; use URI; use URI::file; use Term::ProgressBar; use Term::Prompt qw( prompt ); use Getopt::Long qw( GetOptions ); use Path::Class qw( file ); my $debug = 0; my $progress = 0; my $active = 0; GetOptions( 'd' => \$debug, 'p' => \$progress, 'a' => \$active, ); my $local = shift; my $remote = shift; unless(defined $local && defined $remote) { say STDERR "usage: perl fput.pl [ -d | -p ] [ -a ] local remote"; say STDERR " where local is a local file"; say STDERR " and remote is a URL for a FTP server"; say STDERR " -d (optional) prints FTP commands and responses"; say STDERR " -p (optional) displays a progress bar as the file uploads"; say STDERR " -a (optional) use an active transfer instead of passive"; exit 2; } $local = file($local); $remote = URI->new($remote); unless($remote->scheme eq 'ftp') { say STDERR "only FTP URLs are supported"; exit 2; } unless(defined $remote->password) { $remote->password(prompt('p', 'Password: ', '', '')); say ''; } do { my $from = URI::file->new_abs($local); my $to = $remote->clone; $to->password(undef); say "SRC: ", $from; say "DST: ", $to; }; my $ftp = AnyEvent::FTP::Client->new( passive => $active ? 0 : 1 ); $ftp->on_send(sub { my($cmd, $arguments) = @_; $arguments //= ''; $arguments = 'XXXX' if $cmd eq 'PASS'; say "CLIENT: $cmd $arguments" if $debug; }); $ftp->on_each_response(sub { my $res = shift; if($debug) { say sprintf "SERVER: [ %d ] %s", $res->code, $_ for @{ $res->message }; } }); $ftp->connect($remote->host, $remote->port)->recv; $ftp->login($remote->user, $remote->password)->recv; $ftp->type('I')->recv; if(defined $remote->path) { $ftp->cwd($remote->path)->recv; } open my $fh, '<', $local; binmode $fh; my $buffer; my $count; my $pb; my $xfer = $ftp->stor($local->basename); $xfer->on_open(sub { my $whandle = shift; $pb = Term::ProgressBar->new({ count => -s $fh }) if $progress; $whandle->on_drain(sub { $pb->update($count) if $pb; my $ret = read $fh, $buffer, 1024 * 512; $count += $ret; if($ret > 0) { $whandle->push_write($buffer); } else { $pb->update($count) if $pb; $whandle->push_shutdown; close $fh; } }); }); $xfer->recv; $ftp->quit->recv;
Author: Graham Ollis <plicease@cpan.org>
Contributors:
Ryo Okamoto
Shlomi Fish
José Joaquín Atria
This software is copyright (c) 2017-2021 by Graham Ollis.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.