Skip to main content

kget/ftp/
client.rs

1//! FTP client implementation.
2
3use crate::config::ProxyConfig;
4use crate::optimization::Optimizer;
5use crate::progress::create_progress_bar;
6use crate::utils::print;
7use std::error::Error;
8use std::io::Write;
9use suppaftp::FtpStream;
10use url::Url;
11
12/// FTP file downloader with progress tracking.
13///
14/// Supports:
15/// - Anonymous FTP (use "anonymous" as username)
16/// - Authenticated FTP with username/password in URL
17/// - Binary transfer mode (safe for all file types)
18/// - SOCKS5 proxy connections
19///
20/// # URL Format
21///
22/// ```text
23/// ftp://[user[:password]@]host[:port]/path/to/file
24/// ```
25///
26/// # Example
27///
28/// ```rust,no_run
29/// use kget::ftp::FtpDownloader;
30/// use kget::{ProxyConfig, Optimizer};
31///
32/// // Anonymous download
33/// let dl = FtpDownloader::new(
34///     "ftp://ftp.gnu.org/gnu/emacs/emacs-28.2.tar.gz".to_string(),
35///     "emacs-28.2.tar.gz".to_string(),
36///     false,
37///     ProxyConfig::default(),
38///     Optimizer::new(),
39/// );
40/// dl.download().expect("FTP download failed");
41///
42/// // Authenticated download
43/// let dl = FtpDownloader::new(
44///     "ftp://user:pass@private-server.com/file.zip".to_string(),
45///     "file.zip".to_string(),
46///     false,
47///     ProxyConfig::default(),
48///     Optimizer::new(),
49/// );
50/// dl.download().expect("FTP download failed");
51/// ```
52pub struct FtpDownloader {
53    url: String,
54    output_path: String,
55    quiet_mode: bool,
56    proxy: ProxyConfig,
57    #[allow(dead_code)]
58    optimizer: Optimizer,
59}
60
61impl FtpDownloader {
62    /// Create a new FTP downloader.
63    ///
64    /// # Arguments
65    ///
66    /// * `url` - FTP URL including path to file
67    /// * `output_path` - Local path to save the file
68    /// * `quiet_mode` - Suppress console output
69    /// * `proxy` - Proxy configuration (SOCKS5 only)
70    /// * `optimizer` - Optimizer instance
71    pub fn new(
72        url: String,
73        output_path: String,
74        quiet_mode: bool,
75        proxy: ProxyConfig,
76        optimizer: Optimizer,
77    ) -> Self {
78        Self {
79            url,
80            output_path,
81            quiet_mode,
82            proxy,
83            optimizer,
84        }
85    }
86
87    pub fn download(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
88        let url = Url::parse(&self.url)?;
89        let host = url.host_str().ok_or("Invalid host")?;
90        let port = url.port().unwrap_or(21);
91        let path = url.path();
92
93        print(
94            &format!("Connecting to FTP server {}:{}...", host, port),
95            self.quiet_mode,
96        );
97
98        let mut ftp = if self.proxy.enabled {
99            self.connect_via_proxy(host, port)?
100        } else {
101            FtpStream::connect((host, port))?
102        };
103
104        // url.username() returns "" (empty string) when the URL has no user — fall back to "anonymous"
105        let username = if url.username().is_empty() {
106            "anonymous"
107        } else {
108            url.username()
109        };
110        let password = url.password().unwrap_or("anonymous");
111
112        print(&format!("Logging in as {}...", username), self.quiet_mode);
113        ftp.login(username, password)?;
114
115        ftp.transfer_type(suppaftp::types::FileType::Binary)?;
116
117        let size = ftp.size(path)? as u64;
118
119        let progress = create_progress_bar(
120            self.quiet_mode,
121            format!("Downloading {}", path),
122            Some(size),
123            false,
124        );
125
126        let mut file = std::fs::File::create(&self.output_path)?;
127
128        // Download file
129        let mut downloaded = 0;
130        ftp.retr(path, |reader| {
131            let mut buffer = vec![0; 8192];
132            loop {
133                match reader.read(&mut buffer) {
134                    Ok(0) => break,
135                    Ok(n) => {
136                        file.write_all(&buffer[..n])
137                            .map_err(|e| suppaftp::FtpError::ConnectionError(e))?;
138                        downloaded += n;
139                        progress.set_position(downloaded as u64);
140                    }
141                    Err(e) => return Err(suppaftp::FtpError::ConnectionError(e)),
142                }
143            }
144            Ok(())
145        })?;
146
147        progress.finish();
148        print("Download completed successfully!", self.quiet_mode);
149
150        Ok(())
151    }
152
153    fn connect_via_proxy(
154        &self,
155        host: &str,
156        port: u16,
157    ) -> Result<FtpStream, Box<dyn Error + Send + Sync>> {
158        match self.proxy.proxy_type {
159            crate::config::ProxyType::Http | crate::config::ProxyType::Https => {
160                Err("HTTP/HTTPS proxy not supported for FTP".into())
161            }
162            crate::config::ProxyType::Socks5 => {
163                if let Some(proxy_url) = &self.proxy.url {
164                    let proxy = Url::parse(proxy_url)?;
165                    let proxy_host = proxy.host_str().ok_or("Invalid proxy host")?;
166                    let proxy_port = proxy.port().unwrap_or(1080);
167
168                    let stream =
169                        socks::Socks5Stream::connect((proxy_host, proxy_port), (host, port))?;
170
171                    Ok(FtpStream::connect_with_stream(stream.into_inner())?)
172                } else {
173                    Err("Proxy URL not configured".into())
174                }
175            }
176        }
177    }
178}