SuppaFTP

repository·main·Indexed 41 days ago

https://github.com/veeso/suppaftp

A feature-rich FTP/FTPS client library for Rust providing both synchronous and asynchronous APIs. It supports multiple TLS backends (native-tls and rustls) and async runtimes (tokio and smol). Version ^11 includes a built-in command-line client for interacting with FTP/FTPS servers.

Tokens
15.2K
Snippets
41
Records
60
Agent score
51%

What's inside SuppaFTP

  1. Use the SuppaFTP CLI interactive client

    main

    Once installed, running suppaftp starts an interactive session. You can connect to a host immediately by providing the address as an argument, or enter commands manually at the >> prompt.

    Connection Modes:

    • Standard FTP: CONNECT <addr:port>
    • FTPS (Secure): CONNECT+S <addr:port>

    Logging Levels: Use CLI flags to control output verbosity:

    • --debug: Enables Trace level logging.
    • --verbose: Enables Info level logging.
    # Start interactive session and connect to a host immediately
    suppaftp 127.0.0.1:21
    
    # Start with verbose logging
    suppaftp --verbose
  2. Configure Cargo features for SSL/TLS and Async support

    main

    SuppaFTP uses Cargo features to manage optional functionality like FTPS (SSL/TLS) and asynchronous runtimes.

    SSL/TLS Support

    To enable FTPS, you must select a TLS backend. native-tls is recommended for compatibility. To link libssl statically, use the -vendored variant.

    Async Support

    You must enable either the smol or tokio feature to use asynchronous APIs.

    Feature Combinations

    When combining TLS and Async, you must use the specific combined features to avoid conflicts:

    • Tokio + native-tls: use tokio-async-native-tls (or tokio-async-native-tls-vendored for static linking).
    • Tokio + rustls: use tokio-rustls-aws-lc-rs or tokio-rustls-ring.
    • Smol + native-tls: use smol-async-native-tls (or smol-async-native-tls-vendored for static linking).
    • Smol + rustls: use smol-rustls-aws-lc-rs or smol-rustls-ring.
    # Example: Tokio with native-tls
    suppaftp = { version = "^11", features = ["tokio-async-native-tls"] }
    
    # Example: Sync with rustls
    suppaftp = { version = "^11", features = ["rustls-aws-lc-rs"] }
  3. Install SuppaFTP

    main

    To use SuppaFTP in your Rust project, add it to your Cargo.toml dependencies.

    Note: The current version in the documentation is ^11, but ensure you check for the latest version on Crates.io.

    suppaftp = "^11"
  4. How to use FTPS (Explicit Mode)

    main

    SuppaFTP uses explicit mode for FTPS. You connect to the server normally and then switch to a secure mode using .into_secure(). It is recommended to switch to secure mode before authentication.

    To use native-tls FTPS, ensure the native-tls feature is enabled.

    use suppaftp::{NativeTlsFtpStream, NativeTlsConnector};
    use suppaftp::native_tls::{TlsConnector, TlsStream};
    
    let ftp_stream = NativeTlsFtpStream::connect("test.rebex.net:21").unwrap();
    // Switch to the secure mode
    let mut ftp_stream = ftp_stream.into_secure(NativeTlsConnector::from(TlsConnector::new().unwrap()), "test.rebex.net").unwrap();
    ftp_stream.login("demo", "password").unwrap();
    // Do other secret stuff
    assert!(ftp_stream.quit().is_ok());
  5. Finalize FTP transfers with `TransferStream::finish`

    main

    When using smol with suppaftp, data transfers (uploads and downloads) are managed via a TransferStream. This stream implements AsyncRead and AsyncWrite, but it is not complete just because you have finished reading or writing the data.

    To successfully complete a transfer and verify its outcome, you must call .finish().await on the TransferStream. This method closes the data connection and reads the server's completion reply (e.g., a 226 response) on the control connection.

    Important considerations:

    • Uploads: A transfer is only successful if the write side is closed cleanly. A close error during finish() will result in an FtpError::ConnectionError.
    • Downloads: The server's completion reply is the authoritative source of success, as the payload has already been fully read.
    • Concurrency: While a TransferStream is alive, the client will refuse to open another data connection, returning FtpError::DataConnectionAlreadyOpen.
    • Dropping without finishing: If you simply drop the stream instead of calling finish(), the data socket is closed, but the completion reply is left in the control connection buffer. This reply will be consumed by the next command you issue on the client, which can lead to lost transfer results or unexpected behavior. Always prefer finish().await.
    use smol::io::AsyncWriteExt;
    use suppaftp::smol::AsyncFtpStream;
    
    async fn run() {
        let mut ftp = AsyncFtpStream::connect("127.0.0.1:21").await.unwrap();
        ftp.login("test", "test").await.unwrap();
        
        // Obtain the TransferStream
        let mut upload = ftp.put_with_stream("hello.txt").await.unwrap();
        
        // Perform the data transfer
        upload.write_all(b"hello, world!").await.unwrap();
        
        // CRITICAL: Finalize the transfer to read the server's completion reply
        upload.finish().await.unwrap();
        
        // Now the control connection is ready for the next command
        assert_eq!(ftp.size("hello.txt").await.unwrap(), 13);
    }
  6. How to use Async FTP

    main

    When using the tokio or smol features, use AsyncFtpStream and .await your calls. The API surface is nearly identical to the synchronous version.

    use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector};
    use suppaftp::async_native_tls::{TlsConnector, TlsStream};
    
    let ftp_stream = AsyncFtpStream::connect("test.rebex.net:21").await.unwrap();
    // Switch to the secure mode
    let mut ftp_stream = ftp_stream.into_secure(AsyncNativeTlsConnector::from(TlsConnector::new()), "test.rebex.net").await.unwrap();
    ftp_stream.login("demo", "password").await.unwrap();
    // Do other secret stuff
    assert!(ftp_stream.quit().await.is_ok());
  7. Parse FTP directory listing lines into `File` objects

    main

    You can convert raw strings from FTP directory listings into File instances using several methods.

    Implementations of FromStr and TryFrom<&str> allow you to parse a line by attempting multiple formats in sequence. The parser will try formats in this order:

    1. POSIX
    2. DOS
    3. MLSD
    4. MLST
    // Using parse() via FromStr
    let file: File = "-rw-r--r-- 1 user group 1234 Nov 5 2018 example.txt".parse().unwrap();
    
    // Using try_from()
    let file = File::try_from("04-08-14  03:09PM  8192 omar.txt").unwrap();

    If you know the specific format of the listing, use the ListParser methods directly to avoid the overhead of trial-and-error parsing:

    • ListParser::parse_posix(line)
    • ListParser::parse_dos(line)
    • ListParser::parse_mlsd(line)
    • ListParser::parse_mlst(line)

    Note: Several direct File::from_*_line methods (like from_posix_line) are deprecated since version 7.1.0. Use ListParser instead.

    // Automatic parsing via FromStr
    let file: File = "-rw-r--r-- 1 user group 1234 Nov 5 2018 example.txt".parse().unwrap();
    
    // Explicit parsing via ListParser (preferred over deprecated File methods)
    let file = ListParser::parse_posix("-rw-r--r-- 1 user group 1234 Nov 5 2018 example.txt").unwrap();
  8. Parse FTP directory listings with ListParser

    main

    The ListParser struct provides methods to convert raw string lines from various FTP LIST command formats into File objects. Because FTP server implementations vary, you must choose the parser method that matches the output format of your specific server.

    Supported formats:

    • POSIX: Standard Unix-style listings (e.g., -rw-r--r-- ...). Use parse_posix.
    • DOS: Windows-style listings (e.g., 04-08-14 03:09PM <DIR> ...). Use parse_dos.
    • MLSD/MLST: Modern machine-readable listings (e.g., type=dir;modify=...;). Use parse_mlsd or parse_mlst (they are functionally identical).

    Parsing returns a ParseResult<File>, which is a Result<File, ParseError>.

    use suppaftp::list::{File, ListParser};
    
    // Example: Parsing a POSIX line
    let line = "-rw-r--r-- 1 user group 1234 Nov 5 13:46 example.txt";
    let file = ListParser::parse_posix(line).expect("failed to parse LIST line");
    
    assert_eq!(file.name(), "example.txt");
  9. Configure SSL/TLS support for FTPS

    main

    To enable FTPS, you must enable either the native-tls or rustls feature. native-tls is recommended for better compatibility.

    • For native-tls:
      suppaftp = { version = "^8", features = ["native-tls"] }
    • For rustls (using aws-lc-rs backend):
      suppaftp = { version = "^8", features = ["rustls-aws-lc-rs"] }
    suppaftp = { version = "^8", features = ["native-tls"] }
  10. Configure Async support for SuppaFTP

    main

    To use the asynchronous version of the library, you must enable either the smol or tokio feature.

    • For tokio backend:
      suppaftp = { version = "8", features = ["tokio"] }

    Important: Feature Combinations When combining TLS and Async backends, you must use specific combined features:

    • native-tls + smol $\rightarrow$ smol-async-native-tls
    • native-tls + tokio $\rightarrow$ tokio-async-native-tls
    • rustls + smol $\rightarrow$ smol-rustls-aws-lc-rs (or smol-rustls-ring)

    If you need to link libssl statically with smol or tokio, use the -vendored variants (e.g., smol-async-native-tls-vendored).

    suppaftp = { version = "8", features = ["tokio"] }
  11. Finalize FTP transfers using TransferStream::finish

    main

    When using streaming methods like put_with_stream, append_with_stream, or retr_as_stream, the returned TransferStream must be explicitly finalized to ensure the transfer was successful.

    Calling .finish() closes the data socket and reads the server's completion reply (e.g., 226 or 250) on the control connection. This is the only reliable way to confirm the transfer succeeded. If you do not call .finish(), the stream will attempt to finalize on Drop, but this is a best-effort operation that logs errors instead of returning them, and the transfer outcome may be lost.

    Important:

    • You must call .finish() before issuing any other command on the FTP client.
    • If using buffered writers, ensure you flush() them before calling .finish().
    • While a TransferStream is alive, the client will return FtpError::DataConnectionAlreadyOpen if you attempt to open another data connection.
    use std::io::Write;
    use suppaftp::FtpStream;
    
    let mut ftp = FtpStream::connect("127.0.0.1:21").unwrap();
    ftp.login("test", "test").unwrap();
    
    // Start an upload stream
    let mut upload = ftp.put_with_stream("hello.txt").unwrap();
    
    // Write data to the stream
    upload.write_all(b"hello, world!").unwrap();
    
    // CRITICAL: Finalize the transfer to read the server's completion reply
    // and make the control connection ready for the next command.
    upload.finish().unwrap();
    
    assert_eq!(ftp.size("hello.txt").unwrap(), 13);
  12. Use synchronous FTP with FtpStream

    main

    The FtpStream type provides a synchronous API for interacting with FTP servers. You can connect, login, navigate directories, and transfer files using standard blocking calls.

    use std::io::Cursor;
    use std::str;
    use suppaftp::FtpStream;
    
    fn main() {
        // Create a connection to an FTP server and authenticate to it.
        let mut ftp_stream = FtpStream::connect("127.0.0.1:21").unwrap();
        let _ = ftp_stream.login("username", "password").unwrap();
    
        // Get the current directory
        println!("Current directory: {}", ftp_stream.pwd().unwrap());
    
        // Change directory
        let _ = ftp_stream.cwd("test_data").unwrap();
    
        // Retrieve (GET) a file
        let data = ftp_stream.retr_as_buffer("ftpext-charter.txt").unwrap();
        println!(
            "Read file with contents\n{}\n",
            str::from_utf8(&data.into_inner()).unwrap()
        );
    
        // Store (PUT) a file
        let mut reader = Cursor::new("Hello from the Rust \"ftp\" crate!".as_bytes());
        let _ = ftp_stream.put_file("greeting.txt", &mut reader);
        println!("Successfully wrote greeting.txt");
    
        // Terminate the connection
        let _ = ftp_stream.quit();
    }