use anyhow::{anyhow, Result};
#[derive(Debug, PartialEq)]
pub struct FtpUri {
pub host: String,
pub port: u16,
pub path: String,
pub username: Option<String>,
pub password: Option<String>,
pub is_tls: bool,
}
pub fn parse_ftp_uri(uri: &str) -> Result<FtpUri> {
let (scheme, rest) = uri
.split_once("://")
.ok_or_else(|| anyhow!("invalid FTP URI (no scheme): {uri}"))?;
let is_tls = match scheme {
"ftp" => false,
"ftps" => true,
other => return Err(anyhow!("expected ftp:// or ftps://, got {other}://")),
};
if rest.is_empty() {
return Err(anyhow!("FTP URI has no host: {uri}"));
}
let (authority, path) = rest
.split_once('/')
.map(|(a, p)| (a, format!("/{p}")))
.unwrap_or((rest, "/".to_string()));
if authority.is_empty() {
return Err(anyhow!("FTP URI has empty host: {uri}"));
}
let (userinfo, hostport) = if let Some((u, h)) = authority.split_once('@') {
(Some(u), h)
} else {
(None, authority)
};
let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
let port: u16 = p
.parse()
.map_err(|_| anyhow!("invalid port in FTP URI: {uri}"))?;
(h.to_string(), port)
} else {
(hostport.to_string(), 21u16)
};
if host.is_empty() {
return Err(anyhow!("FTP URI has empty host: {uri}"));
}
let (username, password) = if let Some(ui) = userinfo {
if let Some((u, p)) = ui.split_once(':') {
(Some(u.to_string()), Some(p.to_string()))
} else {
(Some(ui.to_string()), None)
}
} else {
(None, None)
};
Ok(FtpUri { host, port, path, username, password, is_tls })
}
pub fn fetch_ftp_bytes(uri: &str) -> Result<Vec<u8>> {
use std::io::Read;
use suppaftp::FtpStream;
let ftp_uri = parse_ftp_uri(uri)?;
let addr = format!("{}:{}", ftp_uri.host, ftp_uri.port);
let mut stream = FtpStream::connect(&addr)
.map_err(|e| anyhow!("FTP connect to {addr} failed: {e}"))?;
let user = ftp_uri.username.as_deref().unwrap_or("anonymous");
let pass = ftp_uri.password.as_deref().unwrap_or("anonymous@");
stream
.login(user, pass)
.map_err(|e| anyhow!("FTP login failed: {e}"))?;
stream
.transfer_type(suppaftp::types::FileType::Binary)
.map_err(|e| anyhow!("FTP TYPE I failed: {e}"))?;
let mut cursor = stream
.retr_as_buffer(&ftp_uri.path)
.map_err(|e| anyhow!("FTP RETR {} failed: {e}", ftp_uri.path))?;
let mut bytes = Vec::new();
cursor
.read_to_end(&mut bytes)
.map_err(|e| anyhow!("FTP read failed: {e}"))?;
let _ = stream.quit();
Ok(bytes)
}