1use ftp::FtpStream;
2use std::io::Write;
3use std::fs::File;
4
5pub struct FtpClient {
6 ftp_addr: String,
7 ftp_port: u16,
8 user_email: String,
9 token_api: String,
10 user_id: String,
11 ftp: Option<FtpStream>,
12 root_dir: Option<String>,
13}
14
15impl FtpClient {
16 pub fn new(addr: &str, port: u16, email: &str, token: &str, user_id: &str) -> Self {
17 FtpClient {
18 ftp_addr: addr.to_string(),
19 ftp_port: port,
20 user_email: email.to_string(),
21 token_api: token.to_string(),
22 user_id: user_id.to_string(),
23 ftp: None,
24 root_dir: None,
25 }
26 }
27
28 pub fn connect(&mut self) -> Result<(), ftp::FtpError> {
29 let address = format!("{}:{}", self.ftp_addr, self.ftp_port);
30 let mut ftp_stream = FtpStream::connect(address)?;
31 ftp_stream.login(&self.user_email, &self.token_api)?;
32
33 match ftp_stream.cwd(&self.user_id) {
35 Ok(_) => {
36 println!("Directory '{}' exists. Switched into it.", self.user_id);
37 }
38 Err(cwd_error) => {
39 println!("Directory '{}' does not exist. Attempting to create...", self.user_id);
40
41 match ftp_stream.mkdir(&self.user_id) {
43 Ok(_) => {
44 println!("Directory '{}' created successfully.", self.user_id);
45 match ftp_stream.cwd(&self.user_id) {
47 Ok(_) => {
48 println!("Successfully switched into directory '{}'.", self.user_id);
49 }
50 Err(cwd_error2) => {
51 println!("Warning: Created directory '{}' but couldn't switch into it: {:?}", self.user_id, cwd_error2);
52 println!("Continuing without switching to user directory.");
53 }
54 }
55 }
56 Err(mkdir_error) => {
57 println!("Warning: Could not create directory '{}': {:?}", self.user_id, mkdir_error);
58 println!("Original cwd error: {cwd_error:?}");
59 println!("Continuing without user directory - working from root.");
60 }
61 }
62 }
63 }
64
65 let current_dir = ftp_stream.pwd()?;
66 println!("Connected and current directory: {current_dir}");
67
68 self.root_dir = Some(current_dir);
69 self.ftp = Some(ftp_stream);
70 Ok(())
71 }
72
73
74 pub fn disconnect(&mut self) {
75 if let Some(mut ftp) = self.ftp.take() {
76 let _ = ftp.quit(); }
78 self.root_dir = None;
79 }
80
81 pub fn is_connected(&self) -> bool {
82 self.ftp.is_some()
83 }
84
85 pub fn download_file(&mut self, remote_path: &str, local_path: &str) -> Result<(), ftp::FtpError> {
86 let ftp_stream = match &mut self.ftp {
87 Some(ftp) => ftp,
88 None => {
89 return Err(ftp::FtpError::ConnectionError(
90 std::io::Error::new(std::io::ErrorKind::NotConnected, "Not connected to FTP server"),
91 ))
92 }
93 };
94
95 let data = ftp_stream.simple_retr(remote_path)?;
96
97 let mut file = File::create(local_path).map_err(|e| {
98 ftp::FtpError::ConnectionError(e)
99 })?;
100
101 file.write_all(&data.into_inner()).map_err(|e| {
102 ftp::FtpError::ConnectionError(e)
103 })?;
104
105 Ok(())
106 }
107
108 pub fn upload_file(&mut self, local_path: &str, remote_path: &str) -> Result<(), ftp::FtpError> {
109 let ftp_stream = match &mut self.ftp {
110 Some(ftp) => ftp,
111 None => {
112 return Err(ftp::FtpError::ConnectionError(
113 std::io::Error::new(std::io::ErrorKind::NotConnected, "Not connected to FTP server"),
114 ))
115 }
116 };
117
118 let mut file = File::open(local_path).map_err(ftp::FtpError::ConnectionError)?;
119 ftp_stream.put(remote_path, &mut file)?;
120
121 Ok(())
122 }
123 pub fn current_directory(&mut self) -> Result<String, ftp::FtpError> {
124 let ftp_stream = match &mut self.ftp {
125 Some(ftp) => ftp,
126 None => {
127 return Err(ftp::FtpError::ConnectionError(
128 std::io::Error::new(std::io::ErrorKind::NotConnected, "Not connected to FTP server"),
129 ))
130 }
131 };
132
133 ftp_stream.pwd()
134 }
135 pub fn check_if_directory_exists(&mut self, dir: &str) -> Result<bool, ftp::FtpError> {
136 let ftp_stream = match &mut self.ftp {
137 Some(ftp) => ftp,
138 None => {
139 return Err(ftp::FtpError::ConnectionError(
140 std::io::Error::new(std::io::ErrorKind::NotConnected, "Not connected to FTP server"),
141 ))
142 }
143 };
144
145 let original_dir = ftp_stream.pwd()?;
147
148 let exists = ftp_stream.cwd(dir).is_ok();
150
151 let _ = ftp_stream.cwd(&original_dir);
153
154 Ok(exists)
155 }
156
157
158 pub fn make_directory(&mut self, dir_name: &str) -> Result<(), ftp::FtpError> {
159 let ftp_stream = match &mut self.ftp {
160 Some(ftp) => ftp,
161 None => {
162 return Err(ftp::FtpError::ConnectionError(
163 std::io::Error::new(std::io::ErrorKind::NotConnected, "Not connected to FTP server"),
164 ))
165 }
166 };
167
168 ftp_stream.mkdir(dir_name)?;
169 println!("Created directory: {dir_name}");
170
171 Ok(())
172 }
173
174
175 pub fn change_directory(&mut self, dir: &str) -> Result<(), ftp::FtpError> {
176 let ftp_stream = match &mut self.ftp {
177 Some(ftp) => ftp,
178 None => {
179 return Err(ftp::FtpError::ConnectionError(
180 std::io::Error::new(std::io::ErrorKind::NotConnected, "Not connected to FTP server"),
181 ))
182 }
183 };
184 ftp_stream.cwd(dir)?;
185 let current_dir = ftp_stream.pwd()?;
186 self.root_dir = Some(current_dir.clone());
187
188 println!("Changed directory to: {current_dir}");
189
190 Ok(())
191 }
192
193 pub fn remove_directory_recursive(&mut self, dir_path: &str) -> Result<(), ftp::FtpError> {
194 let ftp_stream = self.ftp.as_mut().ok_or_else(|| {
195 ftp::FtpError::ConnectionError(std::io::Error::new(
196 std::io::ErrorKind::NotConnected,
197 "Not connected to FTP server",
198 ))
199 })?;
200
201 Self::delete_recursive(ftp_stream, dir_path)
202 }
203
204 fn delete_recursive(ftp: &mut FtpStream, dir_path: &str) -> Result<(), ftp::FtpError> {
205 println!("Processing directory: {dir_path}");
206
207 let current_dir = ftp.pwd()?;
209
210 let entries = match ftp.nlst(Some(dir_path)) {
211 Ok(entries) => entries,
212 Err(e) => {
213 println!("Could not list directory {dir_path}: {e:?}");
214 return Err(e);
215 }
216 };
217
218 for entry in entries {
219 if entry.ends_with("/.") || entry.ends_with("/..") {
220 continue;
221 }
222
223 let entry_name = if entry.starts_with(dir_path) {
224 entry.strip_prefix(dir_path)
225 .unwrap_or(&entry)
226 .trim_start_matches('/')
227 } else if entry.contains('/') {
228 entry.split('/').next_back().unwrap_or(&entry)
229 } else {
230 &entry
231 };
232
233 if entry_name.is_empty() || entry_name == "." || entry_name == ".." {
234 continue;
235 }
236
237 let full_path = format!("{}/{}", dir_path.trim_end_matches('/'), entry_name);
238
239 match ftp.cwd(&full_path) {
241 Ok(_) => {
242 ftp.cwd(¤t_dir)?;
244 println!("Found subdirectory: {full_path}");
245 Self::delete_recursive(ftp, &full_path)?;
246 }
247 Err(_) => {
248 println!("Attempting to delete file: {full_path}");
249 ftp.rm(&full_path).map(|_| {
250 println!("Deleted file: {full_path}");
251 }).map_err(|e| {
252 println!("Could not delete file {full_path}: {e:?}");
253 e
254 })?;
255 }
256 }
257 }
258
259 println!("Removing directory: {dir_path}");
260 ftp.rmdir(dir_path).map(|_| {
261 println!("Successfully deleted directory: {dir_path}");
262 }).map_err(|e| {
263 println!("Failed to delete directory {dir_path}: {e:?}");
264 e
265 })
266 }
267
268
269 pub fn remove_file(&mut self, path: &str) -> Result<(), ftp::FtpError> {
270 let ftp_stream = self.ftp.as_mut().ok_or_else(|| {
271 ftp::FtpError::ConnectionError(std::io::Error::new(
272 std::io::ErrorKind::NotConnected,
273 "Not connected to FTP server",
274 ))
275 })?;
276
277 ftp_stream.rm(path)
278 }
279
280
281}
282
283impl Drop for FtpClient {
284 fn drop(&mut self) {
285 self.disconnect();
286 }
287}
288
289
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use std::fs::{self, File};
295 use std::io::Write;
296 use std::net::TcpListener;
297 use std::sync::mpsc;
298 use std::thread::{self, JoinHandle};
299 use std::time::{Duration, Instant};
300 use tokio::select;
301 use uuid::Uuid;
302 use unftp_sbe_fs::Filesystem;
303 use libunftp::{auth::AnonymousAuthenticator, ServerBuilder};
304
305 fn spawn_test_ftp_server_with_shutdown_ready() -> (JoinHandle<()>, String, std::sync::mpsc::Sender<()>) {
306
307 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
308 let addr = listener.local_addr().unwrap().to_string();
309 drop(listener);
310
311 let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
312 let (ready_tx, ready_rx) = mpsc::channel::<()>();
313
314 let handle = thread::spawn({
315 let addr_clone = addr.clone();
316 move || {
317 let runtime = tokio::runtime::Runtime::new().unwrap();
318 runtime.block_on(async {
319 let backend_factory = || Filesystem::new(std::env::temp_dir());
320
321 let server = ServerBuilder::new(Box::new(backend_factory))
322 .authenticator(std::sync::Arc::new(AnonymousAuthenticator {}))
323 .build()
324 .unwrap();
325
326 let (async_shutdown_tx, mut async_shutdown_rx) = tokio::sync::mpsc::unbounded_channel();
327 let sync_rx = shutdown_rx;
328
329 tokio::spawn(async move {
330 let _ = tokio::task::spawn_blocking(move || sync_rx.recv()).await;
331 let _ = async_shutdown_tx.send(());
332 });
333
334 let server_task = tokio::spawn({
335 let addr_clone = addr_clone.clone();
336 async move {
337 server.listen(addr_clone).await
338 }
339 });
340
341 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
342 let _ = ready_tx.send(());
343
344 select! {
345 result = server_task => {
346 if let Err(e) = result.unwrap() {
347 eprintln!("FTP Server error: {e}");
348 }
349 }
350 _ = async_shutdown_rx.recv() => {
351 println!("Shutdown signal received, stopping server");
352 }
353 }
354 });
355 }
356 });
357
358 ready_rx.recv().expect("Server failed to start");
359
360 (handle, addr, shutdown_tx)
361 }
362
363 fn wait_for_server_ready(addr: &str) {
364 let addr_parts: Vec<&str> = addr.split(':').collect();
365 let host = addr_parts[0];
366 let port: u16 = addr_parts[1].parse().expect("Invalid port");
367
368 let start_time = Instant::now();
369 let timeout = Duration::from_secs(10);
370
371 while start_time.elapsed() < timeout {
372 let mut test_client = FtpClient::new(host, port, "anonymous", "", "test_user");
373 if test_client.connect().is_ok() {
374 test_client.disconnect();
375 return;
376 }
377 thread::sleep(Duration::from_millis(50));
378 }
379
380 panic!("Server did not become ready within timeout period");
381 }
382
383 #[test]
384 fn test_connection() {
385 let (handle, addr, shutdown_tx) = spawn_test_ftp_server_with_shutdown_ready();
386 wait_for_server_ready(&addr);
387
388 let addr_parts: Vec<&str> = addr.split(':').collect();
389 let host = addr_parts[0];
390 let port: u16 = addr_parts[1].parse().expect("Invalid port");
391
392 let test_credentials = vec![
393 ("anonymous", "", "test_user"),
394 ("test", "test", "test_user"),
395 ("ftp", "", "test_user"),
396 ];
397
398 let mut connection_successful = false;
399 for (user, pass, initial_dir) in test_credentials {
400 let mut client = FtpClient::new(host, port, user, pass, initial_dir);
401
402 match client.connect() {
403 Ok(_) => {
404 assert!(client.is_connected());
405 client.disconnect();
406 assert!(!client.is_connected());
407
408 connection_successful = true;
409 break;
410 }
411 Err(e) => {
412 println!("Failed to connect with {user}/{pass}: {e}");
413 }
414 }
415 }
416
417 shutdown_tx.send(()).expect("Failed to send shutdown");
418 handle.join().expect("Server thread panicked");
419
420 assert!(connection_successful, "Failed to connect with any credentials");
421 }
422
423 #[test]
424 fn test_file_upload_and_download() {
425 let (handle, addr, shutdown_tx) = spawn_test_ftp_server_with_shutdown_ready();
426 wait_for_server_ready(&addr);
427
428 let addr_parts: Vec<&str> = addr.split(':').collect();
429 let host = addr_parts[0];
430 let port: u16 = addr_parts[1].parse().expect("Invalid port");
431
432 let mut client = FtpClient::new(host, port, "anonymous", "", "test_user");
433 client.connect().expect("Failed to connect");
434
435 client.make_directory("upload").ok();
436
437 let local_path = "test_upload.txt";
438 let mut file = File::create(local_path).unwrap();
439 writeln!(file, "Hello FTP test").unwrap();
440
441 let remote_path = "upload/test_upload.txt";
442 client.upload_file(local_path, remote_path).expect("Upload failed");
443
444 let download_path = "downloaded_test_upload.txt";
445 client.download_file(remote_path, download_path).expect("Download failed");
446
447 let content = fs::read_to_string(download_path).unwrap();
448 assert!(content.contains("Hello FTP test"));
449
450 fs::remove_file(local_path).unwrap();
451 fs::remove_file(download_path).unwrap();
452 client.remove_directory_recursive("upload").ok();
453 client.disconnect();
454
455 shutdown_tx.send(()).expect("Failed to send shutdown");
456 handle.join().expect("Server thread panicked");
457 }
458
459 #[test]
460 fn test_make_and_change_directory() {
461 let (handle, addr, shutdown_tx) = spawn_test_ftp_server_with_shutdown_ready();
462 wait_for_server_ready(&addr);
463
464 let addr_parts: Vec<&str> = addr.split(':').collect();
465 let host = addr_parts[0];
466 let port: u16 = addr_parts[1].parse().expect("Invalid port");
467
468 let mut client = FtpClient::new(host, port, "anonymous", "", "test_user");
469 client.connect().expect("Failed to connect");
470 let _ = client.current_directory();
471 let user_root = "upload";
473 client.make_directory(user_root).ok();
474 let _ = client.current_directory();
475
476 let uuid = Uuid::new_v4();
477 let dir = format!("{user_root}/test_dir_{uuid}");
478 client.make_directory(&dir).expect("Failed to create dir");
479
480 println!("Directory Made: {dir}");
481 client.change_directory(&dir).expect("Failed to change dir");
482 let _ = client.current_directory();
483 assert!(client.is_connected());
484 client.disconnect();
485
486 shutdown_tx.send(()).expect("Failed to send shutdown");
487 handle.join().expect("Server thread panicked");
488 }
489
490 #[test]
491 fn test_check_if_directory_exists() {
492 let (handle, addr, shutdown_tx) = spawn_test_ftp_server_with_shutdown_ready();
493 wait_for_server_ready(&addr);
494
495 let addr_parts: Vec<&str> = addr.split(':').collect();
496 let host = addr_parts[0];
497 let port: u16 = addr_parts[1].parse().expect("Invalid port");
498
499 let mut client = FtpClient::new(host, port, "anonymous", "", "test_user");
500 client.connect().expect("Failed to connect");
501
502 let uuid1 = Uuid::new_v4();
503 let uuid2 = Uuid::new_v4();
504
505 let dir_name_1 = format!("test_del_{uuid1}");
506 let dir_name_2 = format!("test_del_{uuid2}");
507 let full_path_1 = format!("upload1/{dir_name_1}");
508
509 client.make_directory("upload1").ok();
510 client.make_directory(&full_path_1).expect("Could not create root dir");
511
512 client.change_directory("upload1");
513
514 assert_eq!(
516 client.check_if_directory_exists(&dir_name_1).expect("Failed to check"),
517 true
518 );
519
520 assert_eq!(
522 client.check_if_directory_exists(&dir_name_2).expect("Failed to check"),
523 false
524 );
525 }
526
527
528
529 #[test]
530 fn test_recursive_delete_directory() {
531 let (handle, addr, shutdown_tx) = spawn_test_ftp_server_with_shutdown_ready();
532 wait_for_server_ready(&addr);
533
534 let addr_parts: Vec<&str> = addr.split(':').collect();
535 let host = addr_parts[0];
536 let port: u16 = addr_parts[1].parse().expect("Invalid port");
537
538 let mut client = FtpClient::new(host, port, "anonymous", "", "test_user");
539 client.connect().expect("Failed to connect to FTP server");
540
541 let uuid = Uuid::new_v4();
543 let root_dir = format!("upload1/test_del_{uuid}");
544 let sub_dir = format!("{root_dir}/nested");
545
546 client.make_directory("upload1").ok();
548 client.make_directory(&root_dir).expect("Could not create root dir");
549 client.make_directory(&sub_dir).expect("Could not create nested dir");
550
551 let temp_dir = std::env::temp_dir();
553 let file1_path = temp_dir.join(format!("ftp_temp_root_{uuid}.txt"));
554 let file2_path = temp_dir.join(format!("ftp_temp_nested_{uuid}.txt"));
555
556 std::fs::write(&file1_path, "Root level file\n").expect("Failed to write temp file1");
557 std::fs::write(&file2_path, "Nested file\n").expect("Failed to write temp file2");
558
559 let remote1 = format!("{root_dir}/file1.txt");
561 let remote2 = format!("{sub_dir}/file2.txt");
562 client.upload_file(file1_path.to_str().unwrap(), &remote1).expect("Upload file1 failed");
563 client.upload_file(file2_path.to_str().unwrap(), &remote2).expect("Upload file2 failed");
564
565 println!("Files uploaded to test directories");
566
567 client.remove_directory_recursive(&root_dir).expect("Recursive deletion failed");
569
570 let _ = std::fs::remove_file(&file1_path);
572 let _ = std::fs::remove_file(&file2_path);
573
574 client.disconnect();
575 println!("Disconnected");
576
577 shutdown_tx.send(()).expect("Failed to send shutdown");
578 handle.join().expect("Server thread panicked");
579 }
580
581}