cftp 0.1.0

A small, fast and highly customizable FTP server library.
Documentation
use std::net::IpAddr;

use crate::{IoFactory, PassiveConn};
use thiserror::Error;
use tokio::net::TcpListener;
use tokio_util::compat::{Compat, TokioAsyncReadCompatExt};

#[derive(Debug, Error)]
pub enum TcpFactoryError {
    #[error("failed to pick unused port for passive connection")]
    NoUnusedPort,
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
}

pub struct TcpFactory {
    listener: TcpListener,
}

impl TcpFactory {
    pub fn new(listener: TcpListener) -> Self {
        Self { listener }
    }

    pub async fn bind(
        addr: IpAddr,
    ) -> Result<PassiveConn<Compat<tokio::net::TcpStream>, TcpFactory>, TcpFactoryError> {
        let port = match openport::pick_random_unused_port() {
            Some(p) => p,
            None => {
                tracing::error!("failed to pick unused port for passive connection");
                return Err(TcpFactoryError::NoUnusedPort);
            }
        };

        let listener = match TcpListener::bind((addr, port)).await {
            Ok(l) => l,
            Err(e) => {
                tracing::error!(port, err = %e, "failed to bind passive connection listener");
                return Err(TcpFactoryError::Io(e));
            }
        };

        let addr = match listener.local_addr() {
            Ok(a) => a,
            Err(e) => {
                tracing::error!(err = %e, "failed to get local address of passive connection listener");
                return Err(TcpFactoryError::Io(e));
            }
        };

        tracing::info!("listening for passive connection on {}", addr);
        Ok(PassiveConn::new(addr, TcpFactory::new(listener)))
    }
}

impl IoFactory for TcpFactory {
    type Io = Compat<tokio::net::TcpStream>;

    async fn create_io(&mut self) -> Option<Self::Io> {
        let (stream, _) = match self.listener.accept().await {
            Ok(s) => s,
            Err(e) => {
                tracing::error!(err = %e, "failed to accept passive connection");
                return None;
            }
        };

        Some(stream.compat())
    }
}