/* -*- mode: c++; indent-tabs-mode: nil -*- */
/*
    QC_FtpClient.qpp

    Qore Programming Language

    Copyright (C) 2003 - 2023 Qore Technologies, s.r.o.

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.

    Note that the Qore library is released under a choice of three open-source
    licenses: MIT (as above), LGPL 2+, or GPL 2+; see README-LICENSE for more
    information.
*/

#include "qore/Qore.h"
#include "qore/intern/ssl_constants.h"
#include "qore/intern/QC_FtpClient.h"
#include "qore/intern/QC_Queue.h"

//! A hash describing a free-form FTP control response
/** @since %Qore 0.9.4
*/
hashdecl FtpResponseInfo {
    //! The integer response code
    int code;

    //! The response message string
    string msg;
}

//! The FtpClient class allows %Qore code to communicate with FTP servers with the FTP and FTPS protocols
/** The constructor takes an optional URL with the following format:\n
    <tt>[(ftp|ftps)://][</tt><em>username</em><tt>[:</tt><em>password</em><tt>]@]</tt><em>hostname</em><tt>[:</tt><em>port</em><tt>]</tt>

    If the URL is not set with the constructor, then the connection parameters must be set with the FtpClient::set*() methods. At the very minimum the hostname must be set. If any path name is given in the URL, it is ignored. See the following table for default URL parameters.

    <b>FtpClient::constructor() Default URL Parameters</b>
    |!Field|!Default Value
    |\c protocol|\c "ftp" (unencrypted)
    |\c username|\c "anonymous"
    |\c password|\c "qore@nohost.com"
    |\c port|\c 21

    Once the URL (at least the hostname) has been set, any method requiring a connection will make an implicit call to FtpClient::connect() to attempt to connect and login to the FTP server before executing the method.

    Objects of this class are capable of making encrypted FTPS connections according to <a href="http://tools.ietf.org/html/rfc4217">RFC-4217</a>. TLS/SSL encrypted control and data connection will be attempted if the protocol is set to \c "ftps" or the FtpClient::setSecure() method is called before connecting.

    Note that \c "sftp", or FTP over ssh, is not supported with this class; FTPS is an extension of the FTP protocol to allow for secure connections; while \c "sftp" is FTP over an encrypted ssh connection. For sftp and ssh support, use the ssh2 module.

    When a data connection is required, by default the following modes are tried in series:
    - \c EPSV (Extended Passive Mode)
    - \c PASV (Passive Mode)
    - \c PORT (Port mode)

    If the FTP server does not support one of these methods, or network conditions do not allow a data connection of any of these types to be established, then an exception is thrown.

    To manually control which modes are tried, see the FtpClient::setModeEPSV(), FtpClient::setModePASV(), and FtpClient::setModePORT() methods.

    This class supports posting network events to a Queue, either events on the control or data channels or both can be monitored. See @ref event_handling for more information.

    The events raised by this object are listed in the following table:

    <b>FtpClient Events</b>
    |!Name|!Value|!Description
    |@ref EVENT_FTP_SEND_MESSAGE|\c 9|Raised immediately before an FTP control message is sent
    |@ref EVENT_FTP_MESSAGE_RECEIVED|\c 10|Raised when an FTP reply is received on the control channel

    @section ftpclient_timeouts FtpClient Timeouts

    FtpClient objects have a default socket I/O timeout value of 60 seconds.  The following methods can be used to set or retrieve the socket I/O timeout value:
    - @ref Qore::FtpClient::getTimeout() "FtpClient::getTimeout()"
    - @ref Qore::FtpClient::setTimeout() "FtpClient::setTimeout()"

    @note This class is not available with the @ref PO_NO_NETWORK parse option.
*/
qclass FtpClient [dom=NETWORK; arg=QoreFtpClientClass* f; public_members=string path];

//! Creates an empty FtpClient object
/**
    @par Example:
    @code{.py}
FtpClient ftp();
    @endcode
*/
FtpClient::constructor() {
    self->setPrivate(CID_FTPCLIENT, new QoreFtpClientClass);
}

//! Creates an FtpClient object and initializes it with a URL
/** It accepts one argument that will set the URL for the FTP connection; the path is ignored in the URL, however the username, password, hostname and port are respected; additionally if the protocol (or scheme) is \c "ftps", the client will attempt to establish a secure connection to the server according to <a href="http://tools.ietf.org/html/rfc4217">RFC-4217</a> when the first connection is established.

    @param url The URL of the server to connect to; must have at least a hostname

    @par Example:
    @code{.py}
FtpClient ftp("ftp://user:pass@example.com:2021");
    @endcode

    @throw UNSUPPORTED-PROTOCOL Only \c "ftp" or \c "ftps" are allowed as the protocol in the URL
    @throw FTP-URL-ERROR No hostname given in the URL
*/
FtpClient::constructor(string url) {
    ReferenceHolder<QoreFtpClientClass> f(new QoreFtpClientClass(url, xsink), xsink);
    if (*xsink) {
        return;
    }
    const char* url_path = f->getUrlPath();
    if (url_path) {
        ValueHolder val(new QoreStringNode(url_path), xsink);
        if (self->setMemberValue("path", QC_FTPCLIENT, *val, xsink)) {
            return;
        }
    }

    self->setPrivate(CID_FTPCLIENT, f.release());
}

//! Throws an exception to prevent copying of objects this class
/**
    @throw FTPCLIENT-COPY-ERROR FtpClient objects cannot be copied
*/
FtpClient::copy() {
   xsink->raiseException("FTPCLIENT-COPY-ERROR", "FtpClient objects cannot be copied");
}

//! Disconnects any remote connection and destroys the object
/**
    @par Events:
    @ref EVENT_CHANNEL_CLOSED, @ref EVENT_DELETED

    @par Example:
    @code{.py}
delete ftp;
    @endcode
*/
FtpClient::destructor() {
   // have to clear callbacks before destroying
   f->cleanup(xsink);
   f->deref(xsink);
}

//! Connects to the FTP server and attempts a login; if any errors occur, an exception is thrown
/** This method does not need to be called explicitly; connections are established implicitly by all methdods that communicate with FTP servers.

    Connection parameters are set in the constructor or in the FtpClient::set*() methods

    @par Events:
    @ref EVENT_CONNECTING, @ref EVENT_CONNECTED, @ref EVENT_HOSTNAME_LOOKUP, @ref EVENT_HOSTNAME_RESOLVED, @ref EVENT_START_SSL, @ref EVENT_SSL_ESTABLISHED, @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.connect();
    @endcode

    @throw FTP-RESPONSE-ERROR Invalid response received from FTP server
    @throw SOCKET-CONNECT-ERROR Cannot establish socket connection to FTP server
    @throw FTP-CONNECT-ERROR no hostname set, FTP server reported an error, etc
    @throw FTP-LOGIN-ERROR Login denied by FTP server
*/
nothing FtpClient::connect() {
    f->connect(xsink);
}

//! Disconnects from an FTP server
/** @par Events:
    @ref EVENT_CHANNEL_CLOSED

    @par Example:
    @code{.py}
ftp.disconnect();
    @endcode
*/
nothing FtpClient::disconnect() {
   f->disconnect();
}

//! Returns a list of files from the FTP server in the server's long format in the current working directory or @ref nothing if the path cannot be found
/** @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @return a list of files from the FTP server in the server's long format in the current working directory or @ref nothing if the path cannot be found

    @par Example:
    @code{.py}
*string str = ftp.list();
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-LIST-ERROR FTP server returned an error in response to the LIST command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::nlst()
*/
*string FtpClient::list() {
    return f->list(0, true, xsink);
}

//! Returns a list of files from the FTP server in the server's long format for the given path or @ref nothing if the path cannot be found
/**
    @param path The path or filename (or filename filter) to list (ex: \c "/tmp/file*.txt")

    @return a list of files from the FTP server in the server's long format for the given path or @ref nothing if the path cannot be found

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
*string str = ftp.list("/tmp/file*.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-LIST-ERROR FTP server returned an error in response to the LIST command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::nlst()
*/
*string FtpClient::list(string path) {
    return f->list(path->c_str(), true, xsink);
}

//! Returns a list of file names from the FTP server in the current working directory or @ref nothing if the path cannot be found
/** @return a list of file names from the FTP server in the current working directory or @ref nothing if the path cannot be found

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
*string str = ftp.nlst();
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-LIST-ERROR FTP server returned an error in response to the NLST command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::list()
*/
*string FtpClient::nlst() {
    return f->list(0, false, xsink);
}

//! Returns a list of file names from the FTP server for the given path or @ref nothing if the path cannot be found
/** @param path The path or filename (or filename filter) to list (ex: \c "/tmp/file*.txt")

    @return a list of file names from the FTP server for the given path or @ref nothing if the path cannot be found

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
*string str = ftp.nlst("/tmp/file*.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-LIST-ERROR FTP server returned an error in response to the LIST command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::list()
*/
*string FtpClient::nlst(string path) {
    return f->list(path->c_str(), false, xsink);
}

//! Returns the server-side current working directory
/** @return the server-side current working directory

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
string str = ftp.pwd();
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-PWD-ERROR FTP server returned an error to the PWD command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
string FtpClient::pwd() {
    return f->pwd(xsink);
}

//! Changes the current working directory on the server
/** @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.cwd("/foo/bar");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-CWD-ERROR FTP server returned an error to the CWD command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::cwd(string path) {
    f->cwd(path->c_str(), xsink);
}

//! Transfers a local file to the FTP server; if any errors occur, an exception is thrown
/** @param local_path The path on the local system of the file to send
    @param remote_path If given, where to save the file on the server (otherwise the local path is used)

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.put("/tmp/file-1.txt", "file.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-FILE-OPEN-ERROR Could not open local file for reading
    @throw FTP-FILE-PUT-ERROR Could not determine file size of local file (stat() failed)
    @throw FTP-PUT-ERROR An error occurred while sending the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::putData()
*/
nothing FtpClient::put(string local_path, *string remote_path) [dom=FILESYSTEM] {
    f->put(local_path->c_str(), remote_path ? remote_path->c_str() : 0, xsink);
}

//! Transfers the content of an @ref InputStream to the FTP server; if any errors occur, an exception is thrown
/** @param is The input stream to send to the FTP server
    @param remote_path The name of the file on the server

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.put(new FileInputStream("/tmp/file-1.txt"), "file.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-FILE-OPEN-ERROR Could not open local file for reading
    @throw FTP-FILE-PUT-ERROR Could not determine file size of local file (stat() failed)
    @throw FTP-PUT-ERROR An error occurred while sending the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::putData()
*/
nothing FtpClient::put(Qore::InputStream[InputStream] is, string remote_path) {
    SimpleRefHolder<InputStream> isHolder(is);
    f->put(is, remote_path->c_str(), xsink);
}

//! Renames/moves a file or directory; if any errors occur, an exception is thrown
/** @param from The original file path on the server to rename/move
    @param to The new path on the server

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.rename("file.txt", "file.txt.orig");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-RENAME-ERROR FTP server returned an error to the \c RNFR or \c RNTO commands

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::rename(string from, string to) {
    f->rename(from->c_str(), to->c_str(), xsink);
}

//! Gets a file from the FTP server and stores it on the local filesystem; if any errors occur, an exception is thrown
/** @param remote_path The path on the server to the file to get
    @param local_path The optional local path for saving the file; if this parameter is not given in the call, then the server path is used for the local path as well

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.get("file.txt", "/tmp/file-1.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-FILE-OPEN-ERROR Could not create the local file
    @throw FTP-GET-ERROR There was an error retrieving the file

    @note
    - see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
    - any existing file with the same name is truncated before writing

    @see FtpClient::getAsString(), FtpClient::getAsData()
*/
nothing FtpClient::get(string remote_path, *string local_path) [dom=FILESYSTEM] {
    f->get(remote_path->c_str(), local_path ? local_path->c_str() : 0, xsink);
}

//! Gets a file from the FTP server and writes it to an OutputStream; if any errors occur, an exception is thrown
/** @param remote_path The path on the server to the file to get
    @param os The output stream to write the file to

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.get("file.txt", new FileOutputStream("/tmp/file-1.txt"));
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-FILE-OPEN-ERROR Could not create the local file
    @throw FTP-GET-ERROR There was an error retrieving the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::getAsString(), FtpClient::getAsData()
    @since %Qore 0.8.13
*/
nothing FtpClient::get(string remote_path, Qore::OutputStream[OutputStream] os) {
    SimpleRefHolder<OutputStream> osHolder(os);
    f->get(remote_path->c_str(), os, xsink);
}

//! Transfers string data to the FTP server and saves it as a file on the remote machine; if any errors occur, an exception is thrown
/** @param data The file data to save on the remote server
    @param remote_path The path on the remote server for the file to save the data under

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.putData(str, "file.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-PUT-ERROR An error occurred while sending the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::putData(string data, string remote_path) {
    f->putData(data->c_str(), data->size(), remote_path->c_str(), xsink);
}

//! Transfers binary data to the FTP server and saves it as a file on the remote machine; if any errors occur, an exception is thrown
/** @param data The file data to save on the remote server
    @param remote_path The path on the remote server for the file to save the data under

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.putData(bin, "file.pdf");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-PUT-ERROR An error occurred while sending the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::putData(binary data, string remote_path) {
    f->putData(data->getPtr(), data->size(), remote_path->c_str(), xsink);
}

//! Gets a file from the FTP server and returns it as a string
/** @param remote_path The path on the server to the file to get
    @param encoding the output encoding for the string; if not given, then the default encoding is used (\c UTF-8)

    @return The file retrieved as a string; if any errors occur an exception is raised

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
string str = ftp.getAsString("file.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-FILE-OPEN-ERROR Could not create the local file
    @throw FTP-GET-ERROR There was an error retrieving the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::getAsBinary() for a similar function returning the file's contents as a binary; see
    FtpClient::get() for a function that will get a remote file and save it on the local filesystem

    @since %Qore 1.3 added the \a encoding parameter
*/
string FtpClient::getAsString(string remote_path, *string encoding) {
    return f->getAsString(xsink, remote_path->c_str(), encoding ? QEM.findCreate(encoding) : QCS_DEFAULT);
}

//! Gets a file from the FTP server and returns it as a binary
/** @param remote_path The path on the server to the file to get

    @return The file data retrieved; if any errors occur an exception is raised

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
binary b = ftp.getAsBinary("file.pdf");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-FILE-OPEN-ERROR Could not create the local file
    @throw FTP-GET-ERROR There was an error retrieving the file

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established

    @see FtpClient::getAsString() for a similar function returning the file's contents as a string; see FtpClient::get() for a function that will get a remote file and save it on the local filesystem
*/
binary FtpClient::getAsBinary(string remote_path) {
    return f->getAsBinary(remote_path->c_str(), xsink);
}

//! Deletes a file from the FTP server; if any errors occur, an exception is thrown
/** @param remote_path The path on the server to the file to delete

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.del("file-2.txt");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-DELETE-ERROR FTP server returned an error to the \c DELE command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::del(string remote_path) {
    f->del(remote_path->c_str(), xsink);
}

//! Creates a directory on the FTP server; if any errors occur, an exception is thrown
/** @param remote_path The path on the server to the directory to create

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.mkdir("tmp");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-MKDIR-ERROR FTP server returned an error to the \c MKD command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::mkdir(string remote_path) {
    f->mkdir(remote_path->c_str(), xsink);
}

//! Removes a directory on the remote FTP server; if any errors occur, an exception is thrown
/** @param remote_path The path on the server to the directory to delete

    @par Events:
    @ref EVENT_FTP_SEND_MESSAGE, @ref EVENT_FTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_PACKET_SENT

    @par Example:
    @code{.py}
ftp.rmdir("tmp");
    @endcode

    @throw FTP-RECEIVE-ERROR Incomplete message received on control port
    @throw FTP-RMDIR-ERROR FTP server returned an error to the \c RMD command

    @note see FtpClient::connect() for additional exceptions that could be thrown when connections are implicitly established
*/
nothing FtpClient::rmdir(string remote_path) {
    f->rmdir(remote_path->c_str(), xsink);
}

//! Sets the user name to use for the next connection
/** @param user the user name to use for the next connection

    @par Example:
    @code{.py}
ftp.setUserName("ftp");
    @endcode
*/
nothing FtpClient::setUserName(string user) {
    f->setUserName(user->c_str());
}

//! Sets the password to use for the next connection
/** @param pass the password to use for the next connection

    @par Example:
    @code{.py}
ftp.setPassword("ftp");
    @endcode
*/
nothing FtpClient::setPassword(string pass) {
    f->setPassword(pass->c_str());
}

//! Sets the hostname or address to use to connect to for the next connection
/**
    @param host the hostname or address to use to connect to for the next connection

    @par Example:
    @code{.py}
ftp.setHostName("example.com");
    @endcode
*/
nothing FtpClient::setHostName(string host) {
    f->setHostName(host->c_str());
}

//! Sets the control port value to use for the next connection (the FTP protocol default is 21)
/** @param port the control port value to use for the next connection (the FTP protocol default is 21); must be \> 0 or an FTPCLIENT-SETPORT-PARAMETER-ERROR exception is raised

    @par Example:
    @code{.py}
ftp.setPort(2021);
    @endcode

    @throw FTPCLIENT-SETPORT-PARAMETER-ERROR port \<= 0 number passed as argument
*/
nothing FtpClient::setPort(int port) {
    if (port <= 0) {
        xsink->raiseException("FTPCLIENT-SETPORT-PARAMETER-ERROR", "expecting positive port number as first "
            "parameter of FtpClient::setPort(softint port); got %d", port);
    } else {
        f->setPort(port);
    }
}

//! Sets the connection and login parameters based on the URL passed as an argument
/** Only \c "ftp" and \c "ftps" protocols (schemes) are accepted; any other protocol (scheme) will cause an exception to be thrown.

    @param url The URL to use for the next connection; the URL must contain at least a hostname

    @par Example:
    @code{.py}
ftp.setURL("ftps://user:pass@example.com:2021");
    @endcode

    @throw UNSUPPORTED-PROTOCOL Only \c "ftp" and "ftps" are allowed as the protocol (scheme) in the URL
    @throw FTP-URL-ERROR No hostname given in the URL
*/
nothing FtpClient::setURL(string url) {
    f->setURL(url, xsink);

    const char* url_path = f->getUrlPath();
    if (url_path) {
        ValueHolder val(new QoreStringNode(url_path), xsink);
        if (self->setMemberValue("path", QC_FTPCLIENT, *val, xsink)) {
            return QoreValue();
        }
    }
}

//! Sets the network address family to use for new connections; use this method to restrict connections to IPv4 or IPv6 connections
/** @param family the network address family to use for new connections; see @ref network_address_family_constants for valid values; use @ref Qore::AF_UNSPEC to automatically choose the family (the default)

    @par Example:
    @code{.py}
ftp.setNetworkFamily(AF_INET);
    @endcode

    @throw FTPCLIENT-SETNETWORKFAMILY-ERROR invalid or unknown network family

    @since %Qore 0.9.0
*/
nothing FtpClient::setNetworkFamily(int family) {
    if (family != AF_INET && family != AF_INET6 && family != AF_UNSPEC) {
        xsink->raiseException("FTPCLIENT-SETNETWORKFAMILY-ERROR", "invalid network family " QLLD, family);
        return QoreValue();
    }
    f->setNetworkFamily(family);
}

//! Returns the network address family to use for new connections
/** @return the network address family to use for new connections; see @ref network_address_family_constants for valid values; @ref Qore::AF_UNSPEC means to automatically choose the family (the default)

    @par Example:
    @code{.py}
int family = ftp.getNetworkFamily();
    @endcode

    @since %Qore 0.9.0
*/
int FtpClient::getNetworkFamily() [flags=CONSTANT] {
    return f->getNetworkFamily();
}

//! Returns the current username value or @ref nothing if none is set
/**
    @return the current username value that will be used for the next connection or @ref nothing if none is set

    @par Example:
    @code{.py}
*string user = ftp.getUserName();
    @endcode
 */
*string FtpClient::getUserName() {
    const char *u = f->getUserName();
    return u ? new QoreStringNode(u) : nullptr;
}

//! Returns information about the local control socket as a hash
/** If the control socket is not open, an exception is thrown

    @par Example:
    @code{.py}
hash<auto> h = ftp.getControlSocketInfo();
    @endcode

    @param host_lookup do a lookup of the hostname; if this is @ref False "False" then \c "hostname" and \c "hostname_desc" are not present in the response hash

    @return a @ref socket_info_hash "hash of information" about the remote end for connected sockets

    @throw SOCKET-NOT-OPEN The socket is not open
    @throw SOCKET-GETSOCKETINFO-ERROR error in getsockname()

    @since %Qore 0.9.0
 */
hash<auto> FtpClient::getControlSocketInfo(bool host_lookup = True) {
    return f->getControlSocketInfo(xsink, host_lookup);
}

//! Returns information about the local data socket as a hash
/** If the data socket is not open, an exception is thrown

    @par Example:
    @code{.py}
hash<auto> h = ftp.getControlSocketInfo();
    @endcode

    @param host_lookup do a lookup of the hostname; if this is @ref False "False" then \c "hostname" and \c "hostname_desc" are not present in the response hash

    @return a @ref socket_info_hash "hash of information" about the remote end for connected sockets

    @throw SOCKET-NOT-OPEN The socket is not open
    @throw SOCKET-GETSOCKETINFO-ERROR error in getsockname()

    @since %Qore 0.9.0
 */
hash<auto> FtpClient::getDataSocketInfo(bool host_lookup = True) {
    return f->getDataSocketInfo(xsink, host_lookup);
}

//! Returns information about the remote end of the control socket as a hash
/** If the control socket is not open, an exception is thrown

    @par Example:
    @code{.py}
hash<auto> h = ftp.getControlPeerInfo();
    @endcode

    @param host_lookup do a lookup of the hostname; if this is @ref False "False" then \c "hostname" and \c "hostname_desc" are not present in the response hash

    @return a @ref socket_info_hash "hash of information" about the remote end for connected sockets

    @throw SOCKET-NOT-OPEN The socket is not open
    @throw SOCKET-GETPEERINFO-ERROR error in getpeername()

    @since %Qore 0.9.0
 */
hash<auto> FtpClient::getControlPeerInfo(bool host_lookup = True) {
    return f->getControlPeerInfo(xsink, host_lookup);
}

//! Returns information about the remote data socket as a hash
/** If the data socket is not open, an exception is thrown

    @par Example:
    @code{.py}
hash<auto> h = ftp.getDataPeerInfo();
    @endcode

    @param host_lookup do a lookup of the hostname; if this is @ref False "False" then \c "hostname" and \c "hostname_desc" are not present in the response hash

    @return a @ref socket_info_hash "hash of information" about the remote end for connected sockets

    @throw SOCKET-NOT-OPEN The socket is not open
    @throw SOCKET-GETPEERINFO-ERROR error in getpeername()

    @since %Qore 0.9.0
 */
hash<auto> FtpClient::getDataPeerInfo(bool host_lookup = True) {
    return f->getDataPeerInfo(xsink, host_lookup);
}

//! Returns the current password value or @ref nothing if none is set
/**
    @return the current password value that will be used for the next connection or @ref nothing if none is set

    @par Example:
    @code{.py}
*string pass = ftp.getPassword();
    @endcode
 */
*string FtpClient::getPassword() {
    const char *p = f->getPassword();
    return p ? new QoreStringNode(p) : nullptr;
}

//! Returns the current hostname value or @ref nothing if none is set
/**
    @return the current hostname value that will be used for the next connection or @ref nothing if none is set

    @par Example:
    @code{.py}
*string host = ftp.getHostName();
    @endcode
 */
*string FtpClient::getHostName() {
    const char *h = f->getHostName();
    return h ? new QoreStringNode(h) : nullptr;
}

//! Retrieves the current connection port value for this object
/**
    @return the current port value that will be used for the next connection

    @par Example:
    @code{.py}
int port = ftp.getPort();
    @endcode
 */
int FtpClient::getPort() {
    return f->getPort();
}

//! Retrieves the current connection URL string for this object
/**
    @return the current connection URL string for this object

    @par Example:
    @code{.py}
string url = ftp.getURL();
    @endcode
 */
string FtpClient::getURL() {
    return f->getURL();
}

//! Make an FTPS connection to the server on the next connect if the argument is @ref True
/** @param secure Make an FTPS connection to the server on the next connect if the argument is @ref True; make a cleartext connection if the argument is @ref False

    @par Example:
    @code{.py}
ftp.setSecure(True);
    @endcode

    @throw SET-SECURE-ERROR this method cannot be called while the control connection is established
 */
nothing FtpClient::setSecure(bool secure = True) {
    if (secure ? f->setSecure() : f->setInsecure()) {
        xsink->raiseException("SET-SECURE-ERROR", "this method cannot be called while the control connection is "
            "established");
    }
}

//! Make a non-encrypted connection to the server on the next connect
/**
    @par Example:
    @code{.py}
ftp.setInsecure();
    @endcode

    @throw SET-INSECURE-ERROR this method cannot be called while the control connection is established
 */
nothing FtpClient::setInsecure() {
    if (f->setInsecure()) {
        xsink->raiseException("SET-INSECURE-ERROR", "this method cannot be called while the control connection is "
            "established");
    }
}

//! Make a non-encrypted data connection to the server on the next connect even if the control connection is secure
/**
    @par Example:
    @code{.py}
ftp.setSecure();
ftp.setInsecureData();
    @endcode

    @throw SET-INSECUREDATA-ERROR this method cannot be called while the control connection is established
 */
nothing FtpClient::setInsecureData() {
    if (f->setInsecureData()) {
        xsink->raiseException("SET-INSECUREDATA-ERROR", "this method cannot be called while the control connection is "
            "established");
    }
}

//! Returns @ref True if the control connection is a secure TLS/SSL connection, @ref False if not
/** @return @ref True if the control connection is a secure TLS/SSL connection, @ref False if not

    @par Example:
    @code{.py}
bool b = ftp.isSecure();
    @endcode
 */
bool FtpClient::isSecure() [flags=CONSTANT] {
    return f->isSecure();
}

//! Returns @ref True if the data connections are secure TLS/SSL connections, @ref False if not
/**
    @return @ref True if the data connections are secure TLS/SSL connections, @ref False if not

    @par Example:
    @code{.py}
bool b = ftp.isDataSecure();
    @endcode

 */
bool FtpClient::isDataSecure() [flags=CONSTANT] {
    return f->isDataSecure();
}

//! Returns the name of the cipher for an encrypted connection or @ref nothing if an encrypted connection is not established
/** @return the name of the cipher for an encrypted connection or @ref nothing if an encrypted connection is not established

    @par Example:
    @code{.py}
if (ftp.isSecure())
    printf("secure connection: %s %s\n", ftp.getSSLCipherName(), ftp.getSSLCipherVersion());
    @endcode
 */
*string FtpClient::getSSLCipherName() [flags=CONSTANT] {
    const char *str = f->getSSLCipherName();
    return str ? new QoreStringNode(str) : nullptr;
}

//! Returns the version of the cipher for an encrypted connection or @ref nothing if an encrypted connection is not established
/** @return the version of the cipher for an encrypted connection or @ref nothing if an encrypted connection is not established

    @par Example:
    @code{.py}
if (ftp.isSecure())
    printf("secure connection: %s %s\n", ftp.getSSLCipherName(), ftp.getSSLCipherVersion());
    @endcode
 */
*string FtpClient::getSSLCipherVersion() [flags=CONSTANT] {
    const char *str = f->getSSLCipherVersion();
    return str ? new QoreStringNode(str) : nullptr;
}

//! Returns a string code giving the result of verifying the remote certificate or @ref nothing if an encrypted connection is not established
/** @return A string code giving the result of verifying the peer's certificate or @ref nothing if a secure connection has not been established. The possible values are found in the keys of the @ref X509_VerificationReasons hash constant. This hash can also be used to generate a textual description of the verification result.

    @par Example:
    @code{.py}
if (ftp.isSecure() && (*string str = ftp.verifyPeerCertificate())) {
    printf("certificate: %s: %s\n", str, X509_VerificationReasons{str});
    @endcode
 */
*string FtpClient::verifyPeerCertificate() [flags=CONSTANT] {
    const char *c = getSSLCVCode(f->verifyPeerCertificate());
    return c ? new QoreStringNode(c) : nullptr;
}

//! Sets the object to automatically try to negotiate the data connections in \c EPSV, \c PASV, and \c PORT modes, in that order
/**
    @par Example:
    @code{.py}
ftp.setModeAuto();
    @endcode
 */
nothing FtpClient::setModeAuto() {
    f->setModeAuto();
}

//! Sets the object to only try to make data connections using \c EPSV (<a href="http://tools.ietf.org/html/rfc2428">RFC-2428</a> extended passive) mode
/**
    @par Example:
    @code{.py}
ftp.setModeEPSV();
    @endcode
 */
nothing FtpClient::setModeEPSV() {
    f->setModeEPSV();
}

//! Sets the object to only try to make data connections using \c PASV (<a href="http://tools.ietf.org/html/rfc959">RFC-959</a> passive) mode
/**
    @par Example:
    @code{.py}
ftp.setModePASV();
    @endcode
 */
nothing FtpClient::setModePASV() {
    f->setModePASV();
}

//! Sets the object to only try to make data connections using \c PORT mode
/**
    @par Example:
    @code{.py}
ftp.setModePORT();
    @endcode
 */
nothing FtpClient::setModePORT() {
    f->setModePORT();
}

//! returns a string for the connection mode: \c "port", \c "pasv", \c "epsv", or \c "auto" if not connected and auto mode is set
/** @par Example:
    @code{.py}
string mode = ftp.getMode();
    @endcode

    @since %Qore 0.8.12
*/
string FtpClient::getMode() [flags=CONSTANT] {
    return new QoreStringNode(f->getMode());
}

//! Clears any Queue object that may be set on the FtpClient object so that @ref event_handling "I/O events" are no longer captured on the object on either the data or control connections
/**
    @par Example:
    @code{.py}
ftp.setEventQueue();
    @endcode
*/
nothing FtpClient::setEventQueue() {
    f->setEventQueue(xsink, nullptr, QoreValue(), false);
}

//! Sets a Queue object to receive FtpClient and Socket events on both the data and control connections
/** @par Example:
    @code{.py}
ftp.setEventQueue(queue);
    @endcode

    @param queue a @ref Qore::Thread::Queue "Queue" object to receive FtpClient and Socket events on both the data and control connections; note that the Queue passed cannot have any maximum size set or a QUEUE-ERROR will be thrown
    @param arg an argument that will be included in each event hash in the \a arg key
    @param with_data if @ref True, then the actual raw data transferred / received is also included in the events

    @throw QUEUE-ERROR the Queue passed has a maximum size set

    @see event_handling for more information

    @since %Qore 0.9.4 added the \a arg and \a with_data options
*/
nothing FtpClient::setEventQueue(Qore::Thread::Queue[Queue] queue, auto arg, *bool with_data) {
    ReferenceHolder<Queue> q(queue, xsink);
    if (queue->getMax() != -1) {
        return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which "
            "could cause internal I/O to freeze in the FtpClient object if used; use a Queue object with no maximum "
            "size for I/O monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");
    }
    // pass reference from QoreObject::getReferencedPrivateData() to function
    f->setEventQueue(xsink, q.release(), arg.refSelf(), with_data);
}

//! Clears any Queue object that may be set on the FtpClient object so that @ref event_handling "I/O events" are no longer captured on the object on the data connection
/**
    @par Example:
    @code{.py}
ftp.setDataEventQueue();
    @endcode
 */
nothing FtpClient::setDataEventQueue() {
    f->setDataEventQueue(xsink, nullptr, QoreValue(), false);
}

//! Sets a Queue object to receive FtpClient and Socket events on the data connection
/** @par Example:
    @code{.py}
ftp.setDataEventQueue(queue);
    @endcode

    @param queue a @ref Qore::Thread::Queue "Queue" object to receive FtpClient and Socket events on the data connection; note that the Queue passed cannot have any maximum size set or a QUEUE-ERROR will be thrown
    @param arg an argument that will be included in each event hash in the \a arg key
    @param with_data if @ref True, then the actual raw data transferred / received is also included in the events

    @throw QUEUE-ERROR the Queue passed has a maximum size set

    @see event_handling for more information

    @since %Qore 0.9.4 added the \a arg and \a with_data options
 */
nothing FtpClient::setDataEventQueue(Qore::Thread::Queue[Queue] queue, auto arg, *bool with_data) {
    ReferenceHolder<Queue> q(queue, xsink);
    if (queue->getMax() != -1) {
        return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which "
            "could cause internal I/O to freeze in the FtpClient object if used; use a Queue object with no maximum "
            "size for I/O monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");
    }
    // pass reference from QoreObject::getReferencedPrivateData() to function
    f->setDataEventQueue(xsink, q.release(), arg.refSelf(), with_data);
}

//! Clears any Queue object that may be set on the FtpClient object so that @ref event_handling "I/O events" are no longer captured on the object on the control connection
/**
    @par Example:
    @code{.py}
ftp.setControlEventQueue();
    @endcode
 */
nothing FtpClient::setControlEventQueue() {
    f->setControlEventQueue(xsink, nullptr, QoreValue(), false);
}

//! Sets a Queue object to receive FtpClient and Socket events on the control connection
/** @par Example:
    @code{.py}
ftp.setControlEventQueue(queue);
    @endcode

    @param queue a @ref Qore::Thread::Queue "Queue" object to receive FtpClient and Socket events on the control connection; note that the Queue passed cannot have any maximum size set or a QUEUE-ERROR will be thrown
    @param arg an argument that will be included in each event hash in the \a arg key
    @param with_data if @ref True, then the actual raw data transferred / received is also included in the events

    @throw QUEUE-ERROR the Queue passed has a maximum size set

    @see event_handling for more information

    @since %Qore 0.9.4 added the \a arg and \a with_data options
*/
nothing FtpClient::setControlEventQueue(Qore::Thread::Queue[Queue] queue, auto arg, *bool with_data) {
    ReferenceHolder<Queue> q(queue, xsink);
    if (queue->getMax() != -1) {
        return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which "
            "could cause internal I/O to freeze in the FtpClient object if used; use a Queue object with no maximum "
            "size for I/O monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");
    }
    // pass reference from QoreObject::getReferencedPrivateData() to function
    f->setControlEventQueue(xsink, q.release(), arg.refSelf(), with_data);
}

//! Removes any warning @ref Qore::Thread::Queue "Queue" object from the Socket
/** @par Example:
    @code{.py}
ftp.clearWarningQueue();
    @endcode

    @see FtpClient::setWarningQueue()

    @since %Qore 0.8.9
 */
nothing FtpClient::clearWarningQueue() {
    f->clearWarningQueue(xsink);
}

//! Sets a @ref Qore::Thread::Queue "Queue" object to receive socket warnings
/** @par Example:
    @code{.py}
ftp.setWarningQueue(5000, 5000, queue, "socket-1");
    @endcode

    @param warning_ms the threshold in milliseconds for individual socket actions (send, receive, connect), if exceeded, a socket warning is placed on the warning queue with the following keys:
    - \c "type": a string with the constant value \c "SOCKET-OPERATION-WARNING"
    - \c "operation": a string giving the operation that caused the warning (ex: \c "connect")
    - \c "us": an integer giving the number of microseconds for the operation
    - \c "timeout": an integer giving the warning threshold in microseconds
    - \c "arg": if any \c "arg" argument is passed to the @ref Qore::FtpClient::setWarningQueue() "FtpClient::setWarningQueue()" method, it will be included in the warning hash here
    @param warning_bs value in bytes per second; if any call has performance below this threshold, a socket warning is placed on the warning queue with the following keys:
    - \c "type": a string with the constant value \c "SOCKET-THROUGHPUT-WARNING"
    - \c "dir": either \c "send" or \c "recv" depending on the direction of the data flow
    - \c "bytes": the amount of bytes sent
    - \c "us": an integer giving the number of microseconds for the operation
    - \c "bytes_sec": a float giving the transfer speed in bytes per second
    - \c "threshold": an integer giving the warning threshold in bytes per second
    - \c "arg": if any \c "arg" argument is passed to the @ref Qore::FtpClient::setWarningQueue() "FtpClient::setWarningQueue()" method, it will be included in the warning hash here
    @param queue the @ref Qore::Thread::Queue "Queue" object to receive warning events
    @param arg an optional argument to be placed in the \c "arg" key in each warning hash (could be used to identify the socket for example)
    @param min_ms the minimum transfer time with a resolution of milliseconds for a transfer to be eligible for triggering a warning; transfers that take less than this period of time are not eligible for raising a warning

    @throw QUEUE-ERROR the Queue passed has a maximum size set
    @throw SOCKET-SETWARNINGQUEUE-ERROR at least one of \a warning_ms and \a warning_bs must be > 0

    @see FtpClient::clearWarningQueue()

    @since
    - %Qore 0.8.9
    - %Qore 0.8.12 the minimum threshold for a warning is a transfer of at least 1024 bytes, smaller transfers will not result in a warning
 */
nothing FtpClient::setWarningQueue(int warning_ms, int warning_bs, Queue[Queue] queue, auto arg, timeout min_ms = 1s) {
    ReferenceHolder<Queue> q(queue, xsink);
    if (queue->getMax() != -1) {
        return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which "
            "could cause internal I/O to freeze in the Socket object if used; use a Queue object with no maximum "
            "size for I/O monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");
    }
    // pass reference from QoreObject::getReferencedPrivateData() to function
    f->setWarningQueue(xsink, warning_ms, warning_bs, q.release(), arg.refSelf(), min_ms);
}

//! Returns performance statistics for the socket
/** @par Example:
    @code{.py}
hash h = ftp.getUsageInfo();
    @endcode

    @return a hash with the following keys:
    - \c "bytes_sent": an integer giving the total amount of bytes sent
    - \c "bytes_recv": an integer giving the total amount of bytes received
    - \c "us_sent": an integer giving the total number of microseconds spent sending data
    - \c "us_recv": an integer giving the total number of microseconds spent receiving data
    - \c "arg": (only if warning values have been set with @ref Qore::FtpClient::setWarningQueue() "FtpClient::setWarningQueue()") the optional argument for warning hashes
    - \c "timeout": (only if warning values have been set with @ref Qore::FtpClient::setWarningQueue() "FtpClient::setWarningQueue()") the warning timeout in microseconds
    - \c "min_throughput": (only if warning values have been set with @ref Qore::FtpClient::setWarningQueue() "FtpClient::setWarningQueue()") the minimum warning throughput in bytes/sec

    @since %Qore 0.8.9

    @see FtpClient::clearStats()
 */
hash FtpClient::getUsageInfo() [flags=CONSTANT] {
    return f->getUsageInfo();
}

//! Clears performance statistics
/** @par Example:
    @code{.py}
ftp.clearStats();
    @endcode

    @since %Qore 0.8.9

    @see FtpClient::getUsageInfo()
*/
FtpClient::clearStats() {
    f->clearStats();
}

//! returns the socket I/O timeout value as an integer in milliseconds
/** @return the socket timeout value in milliseconds

    @since %Qore 0.8.12.3
*/
int FtpClient::getTimeout() [flags=CONSTANT] {
    return f->getTimeout();
}

//! sets the socket I/O timeout value in milliseconds
/** @param timeout_ms the timeout value in milliseconds

    @since %Qore 0.8.12.3
*/
FtpClient::setTimeout(timeout timeout_ms) {
    f->setTimeout(timeout_ms);
}

//! Sends a message on the control connection with a reply on the control connection only
/** @param cmd the FTP command to send
    @param arg the argument for the command

    @return the response from the FTP server

    @note This method is for commands only on the control connection; no data connection is possible with this method

    @since %Qore 0.9.4
*/
hash<FtpResponseInfo> FtpClient::sendControlMessage(string cmd, string arg) {
    return f->sendControlMessage(cmd->c_str(), arg->c_str(), xsink);
}
