I have an FTP client using the suppaftp crate. The client needs to support both plain FTP, and FTPS (over TLS), depending on the protocol specified in a user-supplied URL string.
For the most part things work well, but the challenge is that the type of struct (from the suppaftp crate) that's used for the connection is different based on whether TLS is used. In the no-TLS case it's FtpStream = ImplFtpStream<NoTlsStream>
and in the TLS case it's NativeTlsFtpStream = ImplFtpStream<NativeTlsStream>
.
Both types of stream essentially support the same methods (e.g. .login()
, .quit()
, .nlst()
to get a directory listing, .retr_as_buffer()
to download a file)...but since they're different types, it doesn't seem that I'm able to set up a "generic" FTP stream, and execute these methods on it regardless of whether the stream uses TLS. The crate doesn't appear to expose a common trait that enables this.
So the best I've been able to come up with is the below implementation of a "wrapper struct" that aims to abstract away the TLS vs no-TLS cases. This obviously has a high level of code duplication, as I need separate method calls that handles each of the cases. Is there any way to improve on this? Is this a shortcoming of the crate that I'm using? (it seems to be the only actively maintained FTP client).. Or maybe I just need to live with it?
use std::io::Cursor;
use native_tls::TlsConnector;
use suppaftp::{FtpError, FtpStream, NativeTlsConnector, NativeTlsFtpStream};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FtpConnError {
#[error(transparent)]
FtpError(#[from] FtpError),
#[error("path {0} is not valid")]
PathError(String),
#[error("not connected")]
NotConnected,
#[error(transparent)]
UrlParse(#[from] url::ParseError),
}
pub enum Stream {
Plain(FtpStream),
NativeTls(NativeTlsFtpStream),
}
pub struct FtpConnection {
host: String,
port: u16,
user: String,
password: String,
secure: bool,
base_path: String,
ftp_stream: Option<Stream>,
}
impl FtpConnection {
pub fn new(url: &str) -> Self {
let url = url::Url::parse(url).unwrap();
let host = url.host_str().unwrap().to_string();
let port = url.port().unwrap_or(21);
let user = url.username().to_string();
let password = url.password().unwrap_or("").to_string();
let secure = url.scheme() == "ftps";
let base_path = url
.path()
.strip_prefix('/')
.unwrap_or_else(|| url.path())
.to_owned();
FtpConnection {
host,
port,
user,
password,
secure,
base_path,
ftp_stream: None,
}
}
pub fn connect(&mut self) -> Result<(), FtpConnError> {
let addr = &format!("{}:{}", self.host, self.port);
match self.secure {
false => {
let mut ftp_stream = FtpStream::connect(addr)?;
ftp_stream.login(&self.user, &self.password)?;
ftp_stream.set_passive_nat_workaround(true);
ftp_stream.cwd(&self.base_path)?;
self.ftp_stream = Some(Stream::Plain(ftp_stream));
}
true => {
let mut ftp_stream = NativeTlsFtpStream::connect(addr)?.into_secure(
NativeTlsConnector::from(TlsConnector::new().unwrap()),
&self.host,
)?;
ftp_stream.login(&self.user, &self.password)?;
ftp_stream.set_passive_nat_workaround(true);
ftp_stream.cwd(&self.base_path)?;
self.ftp_stream = Some(Stream::NativeTls(ftp_stream));
}
}
Ok(())
}
pub fn disconnect(&mut self) -> Result<(), FtpConnError> {
match &mut self.ftp_stream {
Some(Stream::Plain(ftp_stream)) => ftp_stream.quit().map_err(Into::into),
Some(Stream::NativeTls(ftp_stream)) => ftp_stream.quit().map_err(Into::into),
None => Ok(()),
}
}
pub fn list_files(&mut self) -> Result<Vec<String>, FtpConnError> {
match &mut self.ftp_stream {
Some(Stream::Plain(ftp_stream)) => ftp_stream.nlst(None).map_err(Into::into),
Some(Stream::NativeTls(ftp_stream)) => ftp_stream.nlst(None).map_err(Into::into),
None => Err(FtpConnError::NotConnected),
}
}
pub fn download_file(&mut self, filename: &str) -> Result<Cursor<Vec<u8>>, FtpConnError> {
match &mut self.ftp_stream {
Some(Stream::Plain(ftp_stream)) => ftp_stream.retr_as_buffer(filename).map_err(Into::into),
Some(Stream::NativeTls(ftp_stream)) => ftp_stream.retr_as_buffer(filename).map_err(Into::into),
None => Err(FtpConnError::NotConnected),
}
}
}