AnyEve...:Client(3pm)

πŸ“¦ version 0.20, in ubuntu stonking

🌸 AnyEvent::FTP::Client(3pm) β€” your fave async FTP bestie 🌸

okay so bestie, welcome to AnyEvent::FTP::Client(3pm) πŸ’– the manual page that's literally about talking to FTP servers without ever blocking your event loop. she's asynchronous, she's smooth, and she's ready to download your tarballs. let's go. ✨


πŸ’– NAME πŸ’–

AnyEvent::FTP::Client β€” Simple asynchronous ftp client. that's it, that's the whole name. lowercase "ftp" included, she said what she said. πŸŽ€

⭐ VERSION ⭐

version 0.20

πŸ¦‹ SYNOPSIS πŸ¦‹

Non blocking example, aka the whole vibe, no waiting around:

 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 β€” and heads up, this is 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;

πŸ’… DESCRIPTION πŸ’…

This class provides an AnyEvent client interface to the File Transfer Protocol (FTP). she's the glow-up wrapper around the whole FTP situation. 🫢

πŸ‘‘ ROLES πŸ‘‘

This class consumes these roles, like accessories she never takes off:

  • AnyEvent::FTP::Role::Event
  • AnyEvent::FTP::Client::Role::ResponseBuffer
  • AnyEvent::FTP::Client::Role::RequestBuffer

πŸ”₯ EVENTS πŸ”₯

For details on the event interface see AnyEvent::FTP::Role::Event. go read that one too, bestie.

βœ‰οΈ send ✨

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";
 });

πŸ’€ error 😭

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. she will tell you what's wrong, fr fr.

 $client->on_error(sub {
   my($message) = @_;
   warn "network error: $message";
 });

πŸšͺ close πŸŽ€

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
 });

🌈 greeting 🌸

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. it's giving "hi, who even are you."

 $client->on_greeting(sub {
   # $res is a AnyEvent::FTP::Client::Response
   my($res) = @_;
   if($res->message->[0] =~ /ProFTPD/)
   {
     # detected a ProFTPD server
   }
 });

πŸ¦‹ each_response ⭐

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";
 });

πŸŽ€ next_response πŸ’œ

Works just like each_response event, but only gets fired for the next response received. one and done, no crumbs left. πŸ’…

🌺 ATTRIBUTES 🌺

⏰ timeout 🧚

Timeout for the initial connection to the FTP server. The default is 30.

🌊 passive 🫢

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. pick your fighter. ✨

⚑ METHODS ⚑

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";
   }
 });

πŸ”Œ connect πŸ’–

 $client->connect(@remote_host);

Connect to the FTP server. The remote host may be specified in one of these ways:

  • $client->connect($host, [ $port ]) β€” The host and port of the remote server. If not specified, the default FTP port will be used (21).
  • $client->connect($uri) β€” 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. no extra steps, she already walked in and sat down. πŸ’…

πŸ” login 🌸

 $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.

πŸ“₯ retr πŸŽ€

 $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:

  • scalar reference β€” The contents of the file will be stored in the scalar referred to by the reference.
 my $local;
 $client->retr('foo.txt', \$local);
  • file handle β€” 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);
  • the name of the local file β€” 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');
  • subroutine reference / callback reference β€” 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);

πŸ“€ stor ✨

 $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:

  • scalar reference β€” 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);
  • file handle β€” The contents of the file will be read from the file handle.
 open my $fh, '<', 'foo.txt';
 binmode $fh;
 $client->stor('foo.txt', $fh);
  • the name of the local file β€” 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');

🎁 stou πŸ’œ

 $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;
 });

πŸ“Ž appe 🌈

 $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);
 });
⚠️ heads up bestie: note that the SIZE command is an extension to FTP, and may not be available on all servers. so don't come for her if it's not there. πŸ’€

πŸ“‹ list 🧚

 $client->list($location)

okay that's the first chunk, bestie β€” we're not done yet, there's more coming so stay tuned πŸ’–βœ¨

Fire the FTP LIST command, bestie πŸ’… and heads up: the results come back to that returned condition variable as a list reference β€” not as an AnyEvent::FTP::Client::Response object. Yes, it's giving special treatment. We love that for us.

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;

✨ nlst ✨

$client->nlst($location);

Lowkey a carbon copy of the list method, except it uses the FTP NLST command instead. The one big glow-up difference? This one hands you filenames only. That's it, that's the vibe. πŸŽ€


✨ rename ✨

$client->rename($from, $to);

Renames the remote file from $from to $to. Under the hood it's using the FTP RNFR and RNTO commands, which means this cute little line:

my $cv = $client->rename($from, $to);

is literally just a shortcut for:

my $cv;
$client->rnfr($from)->cb(sub {
  $cv = $client->rnto($to);
});
πŸ’€ Okay so, plot twist: $cv might not be defined right away. So if you're gonna use that second version, use it with care, bestie. She's spicy like that.

✨ cwd ✨

$client->cwd( $dir );

Change to the given directory on the remote server. Simple, iconic, no notes. πŸ‘‘


✨ pwd ✨

$client->pwd;

Grabs the current working directory on the remote server. And here's the tea β˜• β€” you get just the string representing the directory path, not a whole AnyEvent::FTP::Client::Response object. Straight to the point, no filler. πŸ¦‹


✨ cdup ✨

$client->cdup

Change to the parent directory on the remote server. Usually this is exactly the same as:

$client->cwd('..');

Main character energy, honestly. 🌈


✨ type ✨

$client->type

Sets the transfer type. And bestie, ngl, you almost always want to flip this to binary mode the literal second you log on:

$client->type('I');

Not me reminding you again. Do it. 🫢


✨ rest ✨

$client->rest

This command resumes a download transfer. But like, fr fr β€” you'd typically not call this method directly. Instead you throw a restart option on the retr method and let it handle the whole thing. Let her cook. πŸ“


✨ mkd ✨

$client->mkd( $path );

Create a directory on the remote server. New folder, who dis. πŸŽ€


✨ rmd ✨

$client->rmd( $path );

Remove a directory on the remote server. And you're gonna wanna do this one very deliberately, okay? ✨


✨ help ✨

$client->help;

Gets a list of the commands your server understands. The actual format completely depends on the server, so don't come for me if it looks different every time. πŸ’€


✨ dele ✨

$client->dele( $path );

Delete the file on the remote server. Poof. Gone. Byeeee πŸ‘‹


✨ rnfr ✨

$client->rnfr;

Specify the old name for renaming a file. Peep the rename method if you want the shortcut version. 😌


✨ rnto ✨

$client->rnto;

Specify the new name for renaming a file. Again, the rename method has your shortcut. πŸ’–


✨ noop ✨

$client->noop;

Does absolutely nothing. Zero. Nada. The server just sends an OK reply back. It's giving "I'm just here to vibe." πŸ¦‹


✨ allo ✨

$client->allo( $size );

Sends the FTP ALLO command. Not used by modern FTP servers anymore, bestie β€” its time has passed. See RFC959 for the details if you're curious. πŸ“œ


✨ syst ✨

$client->syst;

Returns the type of operating system the server is running. Nosy in a cute way. πŸ‘€


✨ stru ✨

$client->stru;

Specify the file structure mode. Again β€” not used by modern FTP servers. See RFC959 for the details. 🧚


✨ mode ✨

$client->mode

Specify the transfer mode. Not used by modern FTP servers either. See RFC959 for the details. (The old-school RFCs are lowkey carrying this whole section ngl.)


✨ stat ✨

$client->stat;
$client->stat($path);

Get info about a file or a directory on the remote server. The actual format is totally server dependent, so don't expect consistency, expect ✨chaos✨. πŸ’…


✨ user ✨

$client->user( $username );

Specify the user to log in as. Check the connect or login methods if you want the shortcut instead. 🫢


✨ pass ✨

$client->pass( $pass );

Specify the password to use for login. Same deal β€” the connect or login methods have your shortcut. πŸ”


✨ acct ✨

$client->acct( $acct );

Specify the user's account. Sometimes it's used for authentication and authorization when you log in to certain servers, but honestly it's seldom used today in practice. See RFC959 for the details. πŸŽ€


✨ size ✨

$client->size( $path );

Get the size of the remote file specified by $path. Heads up: this is an extension to the FTP standard specified in RFC3659, so older servers (or ngl, even some newer ones) may not implement it at all. πŸ’€

On success it sends the size of the file instead of the response object. Slay. ✨


✨ mdtm ✨

$client->mdtm( $path );

Get the modification time of the remote file specified by $path. Same warning as above bestie: this is an extension to the FTP standard specified in RFC3659, and older (or even newer) servers may just not have it. πŸ™ˆ


✨ quit ✨

$client->quit;

Send the FTP QUIT command and close the connection to the remote server. Closing the tab on that relationship. πŸ‘‹


✨ site ✨

$client->site;

The site method is your little doorway into site-specific FTP commands. A lot of FTP servers support an extended set of commands through the standard FTP SITE command. 🌸

⚠️ Okay so this is important: this command will not check whether the site commands are actually supported by the remote server. It is entirely up to you to figure out whether you can really use these interfaces yourself. Read the room, check the server. πŸ”₯

$client->site->microsoft

For commands specific to Microsoft's IIS FTP server. See AnyEvent::FTP::Client::Site::Microsoft.

$client->site->net_ftp_server

For commands specific to Net::FTPServer. See AnyEvent::FTP::Client::Site::NetFtpServer.

$client->site->proftpd

For commands specific to proftpd. See AnyEvent::FTP::Client::Site::Proftpd.

✨ EXAMPLES ✨

okay bestie, here's where it gets fun 🌈 these are the long-form examples, and they also ship with the AnyEvent::FTP distribution inside its example directory. no cap.


πŸ¦‹ fget.pl

give it a URL to a file and this script goes and fetches it, then parks it on your local machine. wanna watch the FTP commands and their responses scroll by in real time? hit the -d option. πŸ’…

#!/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;

🌸 fls.pl

same energy, different job πŸ’– this one does a directory listing on an FTP directory URL. pop the -d option on to see the FTP commands and their responses as they happen, and if you want the long form of the file listing, that's the -l option's whole moment.

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 };

πŸŽ€ fput.pl

the glow-up twin of fget.pl ✨ hand it a local filename and a remote FTP URL and it uploads the local file up to the remote. periodt.

#!/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;

πŸ’œ SEE ALSO πŸ’œ

  • AnyEvent::FTP
  • AnyEvent::FTP::Server

πŸ’… AUTHOR πŸ’…

Author: Graham Ollis <plicease@cpan.org>

Contributors:

  • Ryo Okamoto
  • Shlomi Fish
  • JosΓ© JoaquΓ­n Atria

This software is copyright (c) 2017-2022 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.


and that's a wrap on the whole page, bestie 🫢 this is perl v5.40.1 doing its thing, dated 2026-02-16, and it's the user-contributed Perl documentation for AnyEve...:Client(3pm) πŸŽ€ go forth and transfer files, you absolute icon πŸ’–πŸ¦‹βœ¨

πŸ“¦ where this page comes from