1use crate::utils::path as path_utils;
6
7use remotefs::File;
8use remotefs::fs::{
9 FileType, Metadata, ReadStream, RemoteError, RemoteErrorType, RemoteFs, RemoteResult, UnixPex,
10 UnixPexClass, Welcome, WriteStream,
11};
12use std::io::{Read, Write};
13use std::net::{SocketAddr, TcpStream};
14use std::path::{Path, PathBuf};
15use suppaftp::FtpResult;
16#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
17pub use suppaftp::FtpStream;
18#[cfg(feature = "native-tls")]
19use suppaftp::NativeTlsConnector as TlsConnector;
20#[cfg(feature = "native-tls")]
21pub use suppaftp::NativeTlsFtpStream as FtpStream;
22#[cfg(feature = "rustls")]
23use suppaftp::RustlsConnector as TlsConnector;
24#[cfg(feature = "rustls")]
25pub use suppaftp::RustlsFtpStream as FtpStream;
26#[cfg(feature = "native-tls")]
27use suppaftp::native_tls::TlsConnector as NativeTlsConnector;
28#[cfg(feature = "rustls")]
29use suppaftp::rustls::ClientConfig;
30use suppaftp::{
31 FtpError, Status,
32 list::{File as FtpFile, PosixPexQuery},
33 types::{FileType as SuppaFtpFileType, Mode, Response},
34};
35
36pub type PassiveStreamBuilder = dyn Fn(SocketAddr) -> FtpResult<TcpStream> + Send + Sync;
40
41pub struct FtpFs {
43 stream: Option<FtpStream>,
45 hostname: String,
47 port: u16,
48 username: String,
50 password: Option<String>,
51 passive_stream_builder: Option<Box<PassiveStreamBuilder>>,
53 mode: Mode,
55 #[cfg(any(feature = "native-tls", feature = "rustls"))]
56 secure: bool,
58 #[cfg(feature = "native-tls")]
59 accept_invalid_certs: bool,
61 #[cfg(feature = "native-tls")]
62 accept_invalid_hostnames: bool,
64}
65
66impl FtpFs {
67 pub fn new<S: AsRef<str>>(hostname: S, port: u16) -> Self {
69 Self {
70 stream: None,
71 hostname: hostname.as_ref().to_string(),
72 port,
73 username: String::from("anonymous"),
74 password: None,
75 mode: Mode::Passive,
76 passive_stream_builder: None,
77 #[cfg(any(feature = "native-tls", feature = "rustls"))]
78 secure: false,
79 #[cfg(feature = "native-tls")]
80 accept_invalid_certs: false,
81 #[cfg(feature = "native-tls")]
82 accept_invalid_hostnames: false,
83 }
84 }
85
86 pub fn username<S: AsRef<str>>(mut self, username: S) -> Self {
90 self.username = username.as_ref().to_string();
91 self
92 }
93
94 pub fn password<S: AsRef<str>>(mut self, password: S) -> Self {
96 self.password = Some(password.as_ref().to_string());
97 self
98 }
99
100 pub fn active_mode(mut self) -> Self {
102 self.mode = Mode::Active;
103 self
104 }
105
106 pub fn passive_mode(mut self) -> Self {
108 self.mode = Mode::Passive;
109 self
110 }
111
112 #[cfg(feature = "native-tls")]
113 pub fn secure(mut self, accept_invalid_certs: bool, accept_invalid_hostnames: bool) -> Self {
115 self.secure = true;
116 self.accept_invalid_certs = accept_invalid_certs;
117 self.accept_invalid_hostnames = accept_invalid_hostnames;
118 self
119 }
120
121 #[cfg(feature = "rustls")]
122 pub fn secure(mut self) -> Self {
123 self.secure = true;
124 self
125 }
126
127 pub fn passive_stream_builder<F>(mut self, builder: F) -> Self
132 where
133 F: Fn(SocketAddr) -> FtpResult<TcpStream> + Send + Sync + 'static,
134 {
135 self.passive_stream_builder = Some(Box::new(builder));
136 self
137 }
138
139 pub fn stream(&mut self) -> Option<&mut FtpStream> {
143 self.stream.as_mut()
144 }
145
146 fn parse_list_lines(&mut self, path: &Path, lines: Vec<String>) -> Vec<File> {
151 lines
153 .into_iter()
154 .flat_map(FtpFile::try_from)
155 .map(|f| {
156 let mut abs_path: PathBuf = path.to_path_buf();
157 abs_path.push(f.name());
158 let file_type = if f.is_symlink() {
159 FileType::Symlink
160 } else if f.is_directory() {
161 FileType::Directory
162 } else {
163 FileType::File
164 };
165 let metadata = Metadata {
166 accessed: None,
167 created: None,
168 file_type,
169 gid: f.gid(),
170 mode: Some(Self::query_unix_pex(&f)),
171 modified: Some(f.modified()),
172 size: f.size() as u64,
173 symlink: f.symlink().map(|x| path_utils::absolutize(path, x)),
174 uid: None,
175 };
176 File {
177 path: abs_path,
178 metadata,
179 }
180 })
181 .collect()
182 }
183
184 fn query_unix_pex(f: &FtpFile) -> UnixPex {
186 UnixPex::new(
187 UnixPexClass::new(
188 f.can_read(PosixPexQuery::Owner),
189 f.can_write(PosixPexQuery::Owner),
190 f.can_execute(PosixPexQuery::Owner),
191 ),
192 UnixPexClass::new(
193 f.can_read(PosixPexQuery::Group),
194 f.can_write(PosixPexQuery::Group),
195 f.can_execute(PosixPexQuery::Group),
196 ),
197 UnixPexClass::new(
198 f.can_read(PosixPexQuery::Others),
199 f.can_write(PosixPexQuery::Others),
200 f.can_execute(PosixPexQuery::Others),
201 ),
202 )
203 }
204
205 #[cfg(target_os = "windows")]
208 fn resolve(p: &Path) -> PathBuf {
209 use path_slash::PathExt as _;
210 PathBuf::from(p.to_slash_lossy().to_string())
211 }
212
213 #[cfg(target_family = "unix")]
214 fn resolve(p: &Path) -> PathBuf {
215 p.to_path_buf()
216 }
217
218 fn check_connection(&mut self) -> RemoteResult<()> {
219 if self.is_connected() {
220 Ok(())
221 } else {
222 Err(RemoteError::new(RemoteErrorType::NotConnected))
223 }
224 }
225
226 #[cfg(feature = "native-tls")]
227 fn setup_tls_connector(&self) -> RemoteResult<TlsConnector> {
228 NativeTlsConnector::builder()
229 .danger_accept_invalid_certs(self.accept_invalid_certs)
230 .danger_accept_invalid_hostnames(self.accept_invalid_hostnames)
231 .build()
232 .map_err(|e| {
233 error!("Failed to setup TLS stream: {}", e);
234 RemoteError::new_ex(RemoteErrorType::SslError, e)
235 })
236 .map(|x| x.into())
237 }
238
239 #[cfg(feature = "rustls")]
240 fn setup_tls_connector(&self) -> RemoteResult<TlsConnector> {
241 let mut root_store = suppaftp::rustls::RootCertStore::empty();
242 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
243 rustls_pki_types::TrustAnchor {
244 subject: ta.subject.clone(),
245 subject_public_key_info: ta.subject_public_key_info.clone(),
246 name_constraints: ta.name_constraints.clone(),
247 }
248 }));
249 Ok(std::sync::Arc::new(
250 ClientConfig::builder()
251 .with_root_certificates(root_store)
252 .with_no_client_auth(),
253 )
254 .into())
255 }
256}
257
258impl RemoteFs for FtpFs {
259 fn connect(&mut self) -> RemoteResult<Welcome> {
260 info!("Connecting to {}:{}", self.hostname, self.port);
261 let mut stream =
262 FtpStream::connect(format!("{}:{}", self.hostname, self.port)).map_err(|e| {
263 error!("Failed to connect to remote server: {}", e);
264 RemoteError::new_ex(RemoteErrorType::ConnectionError, e)
265 })?;
266
267 if let Some(builder) = self.passive_stream_builder.take() {
269 debug!("Setting up a custom passive stream builder");
270 stream = stream.passive_stream_builder(builder);
271 };
272
273 #[cfg(any(feature = "native-tls", feature = "rustls"))]
275 if self.secure {
276 debug!("Setting up TLS stream...");
277 #[cfg(feature = "native-tls")]
278 trace!("Accept invalid certs: {}", self.accept_invalid_certs);
279 #[cfg(feature = "native-tls")]
280 trace!(
281 "Accept invalid hostnames: {}",
282 self.accept_invalid_hostnames
283 );
284 stream = stream
285 .into_secure(self.setup_tls_connector()?, self.hostname.as_str())
286 .map_err(|e| {
287 error!("Failed to negotiate TLS with server: {}", e);
288 RemoteError::new_ex(RemoteErrorType::SslError, e)
289 })?;
290 debug!("TLS handshake OK!");
291 }
292 debug!("Signin in as {}", self.username);
294 stream
295 .login(
296 self.username.as_str(),
297 self.password.as_deref().unwrap_or(""),
298 )
299 .map_err(|e| {
300 error!("Authentication failed: {}", e);
301 RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e)
302 })?;
303 trace!("Setting transfer type to Binary");
304 stream
305 .transfer_type(SuppaFtpFileType::Binary)
306 .map_err(|e| {
307 error!("Failed to set transfer type to Binary: {}", e);
308 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
309 })?;
310 info!("Connection established!");
311 let welcome = Welcome::default().banner(stream.get_welcome_msg().map(|x| x.to_string()));
312 self.stream = Some(stream);
313 Ok(welcome)
314 }
315
316 fn disconnect(&mut self) -> RemoteResult<()> {
317 info!("Disconnecting from FTP server...");
318 self.check_connection()?;
319 let stream = self.stream.as_mut().unwrap();
320 stream.quit().map_err(|e| {
321 error!("Failed to disconnect from remote: {}", e);
322 RemoteError::new_ex(RemoteErrorType::ConnectionError, e)
323 })?;
324 self.stream = None;
325 Ok(())
326 }
327
328 fn is_connected(&mut self) -> bool {
329 self.stream.is_some()
330 }
331
332 fn pwd(&mut self) -> RemoteResult<PathBuf> {
333 debug!("Getting working directory...");
334 self.check_connection()?;
335 let stream = self.stream.as_mut().unwrap();
336 stream.pwd().map(PathBuf::from).map_err(|e| {
337 error!("Pwd failed: {}", e);
338 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
339 })
340 }
341
342 fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
343 debug!("Changing working directory to {}", dir.display());
344 self.check_connection()?;
345 let dir: PathBuf = Self::resolve(dir);
346 let stream = self.stream.as_mut().unwrap();
347 stream
348 .cwd(dir.as_path().to_string_lossy())
349 .map(|_| dir)
350 .map_err(|e| {
351 error!("Failed to change directory: {}", e);
352 RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, e)
353 })
354 }
355
356 fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
357 debug!("Getting list entries for {}", path.display());
358 self.check_connection()?;
359 let path: PathBuf = Self::resolve(path);
360 let stream = self.stream.as_mut().unwrap();
361 stream
362 .list(Some(&path.as_path().to_string_lossy()))
363 .map(|files| self.parse_list_lines(path.as_path(), files))
364 .map_err(|e| {
365 error!("Failed to list directory: {}", e);
366 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
367 })
368 }
369
370 fn stat(&mut self, path: &Path) -> RemoteResult<File> {
371 debug!("Getting file information for {}", path.display());
372 self.check_connection()?;
373 let wrkdir = self.pwd()?;
375 let path = Self::resolve(path);
376 let path = path_utils::absolutize(wrkdir.as_path(), path.as_path());
377 let parent = match path.parent() {
378 Some(p) => p,
379 None => {
380 warn!("{} has no parent: returning root", path.display());
382 return Ok(File {
383 path: PathBuf::from("/"),
384 metadata: Metadata::default().file_type(FileType::Directory),
385 });
386 }
387 };
388 trace!("Listing entries for stat path file: {}", parent.display());
389 let entries = self.list_dir(parent)?;
390 let target = entries.into_iter().find(|x| x.path() == path.as_path());
392 match target {
393 None => {
394 error!("Could not find file; no such file or directory");
395 Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory))
396 }
397 Some(e) => Ok(e),
398 }
399 }
400
401 fn setstat(&mut self, _path: &Path, _metadata: Metadata) -> RemoteResult<()> {
402 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
403 }
404
405 fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
406 debug!("Checking whether {} exists", path.display());
407 match self.stat(path) {
408 Ok(_) => Ok(true),
409 Err(RemoteError {
410 kind: RemoteErrorType::NoSuchFileOrDirectory,
411 ..
412 }) => Ok(false),
413 Err(err) => Err(err),
414 }
415 }
416
417 fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
418 debug!("Removing file {}", path.display());
419 self.check_connection()?;
420 let path = Self::resolve(path);
421 let stream = self.stream.as_mut().unwrap();
422 stream.rm(path.as_path().to_string_lossy()).map_err(|e| {
423 error!("Failed to remove file {}", e);
424 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
425 })
426 }
427
428 fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
429 debug!("Removing file {}", path.display());
430 self.check_connection()?;
431 let path = Self::resolve(path);
432 let stream = self.stream.as_mut().unwrap();
433 stream.rmdir(path.as_path().to_string_lossy()).map_err(|e| {
434 error!("Failed to remove directory {}", e);
435 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
436 })
437 }
438
439 fn create_dir(&mut self, path: &Path, _mode: UnixPex) -> RemoteResult<()> {
440 debug!("Trying to create directory {}", path.display());
441 self.check_connection()?;
442 let path = Self::resolve(path);
443 let stream = self.stream.as_mut().unwrap();
444 match stream.mkdir(path.as_path().to_string_lossy()) {
445 Ok(_) => Ok(()),
446 Err(FtpError::UnexpectedResponse(Response {
447 status: Status::FileUnavailable,
448 ..
449 })) => {
450 error!("Failed to create directory: directory already exists");
451 Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists))
452 }
453 Err(e) => {
454 error!("Failed to create directory: {}", e);
455 Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, e))
456 }
457 }
458 }
459
460 fn symlink(&mut self, _path: &Path, _target: &Path) -> RemoteResult<()> {
461 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
462 }
463
464 fn copy(&mut self, _src: &Path, _dest: &Path) -> RemoteResult<()> {
465 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
466 }
467
468 fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
469 debug!("Trying to rename {} to {}", src.display(), dest.display());
470 self.check_connection()?;
471 let src = Self::resolve(src);
472 let dest = Self::resolve(dest);
473 let stream = self.stream.as_mut().unwrap();
474 stream
475 .rename(
476 &src.as_path().to_string_lossy(),
477 &dest.as_path().to_string_lossy(),
478 )
479 .map_err(|e| {
480 error!("Failed to rename file: {}", e);
481 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
482 })
483 }
484
485 fn exec(&mut self, _cmd: &str) -> RemoteResult<(u32, String)> {
486 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
487 }
488
489 fn append(&mut self, path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
490 debug!("Opening {} for append", path.display());
491 self.check_connection()?;
492 let path = Self::resolve(path);
493 let stream = self.stream.as_mut().unwrap();
494 stream
495 .append_with_stream(path.as_path().to_string_lossy())
496 .map(|x| Box::new(x) as Box<dyn Write + Send>)
497 .map(WriteStream::from)
498 .map_err(|e| {
499 error!("Failed to open file: {}", e);
500 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
501 })
502 }
503
504 fn create(&mut self, path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
505 debug!("Opening {} for write", path.display());
506 self.check_connection()?;
507 let path = Self::resolve(path);
508 let stream = self.stream.as_mut().unwrap();
509 stream
510 .put_with_stream(path.as_path().to_string_lossy())
511 .map(|x| Box::new(x) as Box<dyn Write + Send>)
512 .map(WriteStream::from)
513 .map_err(|e| {
514 error!("Failed to open file: {}", e);
515 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
516 })
517 }
518
519 fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
520 debug!("Opening {} for read", path.display());
521 self.check_connection()?;
522 let path = Self::resolve(path);
523 let stream = self.stream.as_mut().unwrap();
524 stream
525 .retr_as_stream(path.as_path().to_string_lossy())
526 .map(|x| Box::new(x) as Box<dyn Read + Send>)
527 .map(ReadStream::from)
528 .map_err(|e| {
529 error!("Failed to open file: {}", e);
530 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
531 })
532 }
533
534 fn on_read(&mut self, readable: ReadStream) -> RemoteResult<()> {
535 debug!("Finalizing read stream");
536 self.check_connection()?;
537 let stream = self.stream.as_mut().unwrap();
538 stream.finalize_retr_stream(readable).map_err(|e| {
539 error!("Failed to finalize read stream: {}", e);
540 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
541 })
542 }
543
544 fn on_written(&mut self, writable: WriteStream) -> RemoteResult<()> {
545 debug!("Finalizing write stream");
546 self.check_connection()?;
547 let stream = self.stream.as_mut().unwrap();
548 stream.finalize_put_stream(writable).map_err(|e| {
549 error!("Failed to finalize write stream: {}", e);
550 RemoteError::new_ex(RemoteErrorType::ProtocolError, e)
551 })
552 }
553}
554
555#[cfg(test)]
556mod test {
557
558 use crate::test_container::SyncPureFtpRunner;
559
560 use super::*;
561
562 use pretty_assertions::assert_eq;
563
564 use std::{io::Cursor, sync::Arc};
565
566 #[test]
567 fn should_initialize_ftp_filesystem() {
568 let client = FtpFs::new("127.0.0.1", 21);
569 assert!(client.stream.is_none());
570 assert_eq!(client.hostname.as_str(), "127.0.0.1");
571 assert_eq!(client.port, 21);
572 assert_eq!(client.username.as_str(), "anonymous");
573 assert!(client.password.is_none());
574 assert_eq!(client.mode, Mode::Passive);
575 #[cfg(any(feature = "native-tls", feature = "rustls"))]
576 assert_eq!(client.secure, false);
577 #[cfg(feature = "native-tls")]
578 assert_eq!(client.accept_invalid_certs, false);
579 #[cfg(feature = "native-tls")]
580 assert_eq!(client.accept_invalid_hostnames, false);
581 }
582
583 #[test]
584 fn should_build_ftp_filesystem() {
585 let client = FtpFs::new("127.0.0.1", 21)
586 .username("test")
587 .password("omar")
588 .passive_mode()
589 .active_mode();
590 assert!(client.stream.is_none());
591 assert_eq!(client.hostname.as_str(), "127.0.0.1");
592 assert_eq!(client.port, 21);
593 assert_eq!(client.username.as_str(), "test");
594 assert_eq!(client.password.as_deref().unwrap(), "omar");
595 assert_eq!(client.mode, Mode::Active);
596 }
597
598 #[test]
599 #[cfg(any(feature = "native-tls", feature = "rustls"))]
600 fn should_build_secure_ftp_filesystem() {
601 #[cfg(feature = "native-tls")]
602 let client = FtpFs::new("127.0.0.1", 21)
603 .username("test")
604 .password("omar")
605 .secure(true, true)
606 .passive_mode()
607 .active_mode();
608 #[cfg(feature = "rustls")]
609 let client = FtpFs::new("127.0.0.1", 21)
610 .username("test")
611 .password("omar")
612 .secure()
613 .passive_mode()
614 .active_mode();
615 assert!(client.stream.is_none());
616 assert_eq!(client.hostname.as_str(), "127.0.0.1");
617 assert_eq!(client.port, 21);
618 assert_eq!(client.username.as_str(), "test");
619 assert_eq!(client.password.as_deref().unwrap(), "omar");
620 assert_eq!(client.mode, Mode::Active);
621 assert_eq!(client.secure, true);
622 #[cfg(feature = "native-tls")]
623 assert_eq!(client.accept_invalid_certs, true);
624 #[cfg(feature = "native-tls")]
625 assert_eq!(client.accept_invalid_hostnames, true);
626 }
627
628 #[test]
629 fn should_append_to_file() {
630 with_client(|client| {
631 let p = Path::new("a.txt");
633 let file_data = "test data\n";
634 let reader = Cursor::new(file_data.as_bytes());
635 assert_eq!(
636 client
637 .create_file(p, &Metadata::default(), Box::new(reader))
638 .ok()
639 .unwrap(),
640 10
641 );
642 assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
644 let file_data = "Hello, world!\n";
646 let reader = Cursor::new(file_data.as_bytes());
647 assert_eq!(
648 client
649 .append_file(p, &Metadata::default(), Box::new(reader))
650 .ok()
651 .unwrap(),
652 14
653 );
654 assert_eq!(client.stat(p).ok().unwrap().metadata().size, 24);
655 });
656 }
657
658 #[test]
659 fn should_not_append_to_file() {
660 with_client(|client| {
661 let p = Path::new("/tmp/aaaaaaa/hbbbbb/a.txt");
663 let file_data = "Hello, world!\n";
665 let reader = Cursor::new(file_data.as_bytes());
666 assert!(
667 client
668 .append_file(p, &Metadata::default(), Box::new(reader))
669 .is_err()
670 );
671 });
672 }
673
674 #[test]
675 fn should_change_directory() {
676 with_client(|client| {
677 let pwd = client.pwd().ok().unwrap();
678 assert!(client.change_dir(Path::new("/")).is_ok());
679 assert!(client.change_dir(pwd.as_path()).is_ok());
680 });
681 }
682
683 #[test]
684 fn should_not_change_directory() {
685 with_client(|client| {
686 assert!(
687 client
688 .change_dir(Path::new("/tmp/sdfghjuireghiuergh/useghiyuwegh"))
689 .is_err()
690 );
691 });
692 }
693
694 #[test]
695 fn should_not_copy_file() {
696 with_client(|client| {
697 assert!(client.copy(Path::new("a.txt"), Path::new("b.txt")).is_err());
698 });
699 }
700
701 #[test]
702 fn should_create_directory() {
703 with_client(|client| {
704 assert!(
706 client
707 .create_dir(Path::new("mydir"), UnixPex::from(0o755))
708 .is_ok()
709 );
710 });
711 }
712
713 #[test]
714 fn should_not_create_directory_cause_already_exists() {
715 with_client(|client| {
716 assert!(
718 client
719 .create_dir(Path::new("mydir"), UnixPex::from(0o755))
720 .is_ok()
721 );
722 assert_eq!(
723 client
724 .create_dir(Path::new("mydir"), UnixPex::from(0o755))
725 .err()
726 .unwrap()
727 .kind,
728 RemoteErrorType::DirectoryAlreadyExists
729 );
730 });
731 }
732
733 #[test]
734 fn should_not_create_directory() {
735 with_client(|client| {
736 assert!(
738 client
739 .create_dir(
740 Path::new("/tmp/werfgjwerughjwurih/iwerjghiwgui"),
741 UnixPex::from(0o755)
742 )
743 .is_err()
744 );
745 });
746 }
747
748 #[test]
749 fn should_create_file() {
750 with_client(|client| {
751 let p = Path::new("a.txt");
753 let file_data = "test data\n";
754 let reader = Cursor::new(file_data.as_bytes());
755 assert_eq!(
756 client
757 .create_file(p, &Metadata::default(), Box::new(reader))
758 .ok()
759 .unwrap(),
760 10
761 );
762 assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
764 });
765 }
766
767 #[test]
768 fn should_not_create_file() {
769 with_client(|client| {
770 let p = Path::new("/tmp/ahsufhauiefhuiashf/hfhfhfhf");
772 let file_data = "test data\n";
773 let reader = Cursor::new(file_data.as_bytes());
774 assert!(
775 client
776 .create_file(p, &Metadata::default(), Box::new(reader))
777 .is_err()
778 );
779 });
780 }
781
782 #[test]
783 fn should_not_exec_command() {
784 with_client(|client| {
785 assert!(client.exec("echo 5").is_err());
787 });
788 }
789
790 #[test]
791 fn should_tell_whether_file_exists() {
792 with_client(|client| {
793 let p = Path::new("a.txt");
795 let file_data = "test data\n";
796 let reader = Cursor::new(file_data.as_bytes());
797 assert!(
798 client
799 .create_file(p, &Metadata::default(), Box::new(reader))
800 .is_ok()
801 );
802 assert_eq!(client.exists(p).ok().unwrap(), true);
804 assert_eq!(client.exists(Path::new("b.txt")).ok().unwrap(), false);
805 assert_eq!(
806 client.exists(Path::new("/tmp/ppppp/bhhrhu")).ok().unwrap(),
807 false
808 );
809 });
810 }
811
812 #[test]
813 fn should_list_dir() {
814 with_client(|client| {
815 let wrkdir = client.pwd().ok().unwrap();
817 let p = Path::new("a.txt");
818 let file_data = "test data\n";
819 let reader = Cursor::new(file_data.as_bytes());
820 assert!(
821 client
822 .create_file(p, &Metadata::default(), Box::new(reader))
823 .is_ok()
824 );
825 let file = client
827 .list_dir(wrkdir.as_path())
828 .ok()
829 .unwrap()
830 .get(0)
831 .unwrap()
832 .clone();
833 assert_eq!(file.name().as_str(), "a.txt");
834 let mut expected_path = wrkdir;
835 expected_path.push(p);
836 assert_eq!(file.path.as_path(), expected_path.as_path());
837 assert_eq!(file.extension().as_deref().unwrap(), "txt");
838 assert!(file.is_file());
839 assert_eq!(file.metadata.size, 10);
840 assert_eq!(file.metadata.mode.unwrap(), UnixPex::from(0o644));
841 });
842 }
843
844 #[test]
845 fn should_move_file() {
846 with_client(|client| {
847 let p = Path::new("a.txt");
849 let file_data = "test data\n";
850 let reader = Cursor::new(file_data.as_bytes());
851 assert!(
852 client
853 .create_file(p, &Metadata::default(), Box::new(reader))
854 .is_ok()
855 );
856 let dest = Path::new("b.txt");
858 assert!(client.mov(p, dest).is_ok());
859 assert_eq!(client.exists(p).ok().unwrap(), false);
860 assert_eq!(client.exists(dest).ok().unwrap(), true);
861 });
862 }
863
864 #[test]
865 fn should_not_move_file() {
866 with_client(|client| {
867 let p = Path::new("a.txt");
869 let file_data = "test data\n";
870 let reader = Cursor::new(file_data.as_bytes());
871 assert!(
872 client
873 .create_file(p, &Metadata::default(), Box::new(reader))
874 .is_ok()
875 );
876 let dest = Path::new("/tmp/wuefhiwuerfh/whjhh/b.txt");
878 assert!(client.mov(p, dest).is_err());
879 assert!(
880 client
881 .mov(Path::new("/tmp/wuefhiwuerfh/whjhh/b.txt"), p)
882 .is_err()
883 );
884 });
885 }
886
887 #[test]
888 fn should_open_file() {
889 with_client(|client| {
890 let p = Path::new("a.txt");
892 let file_data = "test data\n";
893 let reader = Cursor::new(file_data.as_bytes());
894 assert!(
895 client
896 .create_file(p, &Metadata::default(), Box::new(reader))
897 .is_ok()
898 );
899 let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
901 assert!(client.open_file(p, buffer).is_ok());
902 });
903 }
904
905 #[test]
906 fn should_not_open_file() {
907 with_client(|client| {
908 let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
910 assert!(
911 client
912 .open_file(Path::new("/tmp/aashafb/hhh"), buffer)
913 .is_err()
914 );
915 });
916 }
917
918 #[test]
919 fn should_print_working_directory() {
920 with_client(|client| {
921 assert!(client.pwd().is_ok());
922 });
923 }
924
925 #[test]
926 fn should_remove_dir_all() {
927 with_client(|client| {
928 let mut dir_path = client.pwd().ok().unwrap();
930 dir_path.push(Path::new("test/"));
931 assert!(
932 client
933 .create_dir(dir_path.as_path(), UnixPex::from(0o775))
934 .is_ok()
935 );
936 let mut file_path = dir_path.clone();
938 file_path.push(Path::new("a.txt"));
939 let file_data = "test data\n";
940 let reader = Cursor::new(file_data.as_bytes());
941 assert!(
942 client
943 .create_file(file_path.as_path(), &Metadata::default(), Box::new(reader))
944 .is_ok()
945 );
946 assert!(client.remove_dir_all(dir_path.as_path()).is_ok());
948 });
949 }
950
951 #[test]
952 fn should_not_remove_dir_all() {
953 with_client(|client| {
954 assert!(
956 client
957 .remove_dir_all(Path::new("/tmp/aaaaaa/asuhi"))
958 .is_err()
959 );
960 });
961 }
962
963 #[test]
964 fn should_remove_dir() {
965 with_client(|client| {
966 let mut dir_path = client.pwd().ok().unwrap();
968 dir_path.push(Path::new("test/"));
969 assert!(
970 client
971 .create_dir(dir_path.as_path(), UnixPex::from(0o775))
972 .is_ok()
973 );
974 assert!(client.remove_dir(dir_path.as_path()).is_ok());
975 });
976 }
977
978 #[test]
979 fn should_not_remove_dir() {
980 with_client(|client| {
981 let mut dir_path = client.pwd().ok().unwrap();
983 dir_path.push(Path::new("test/"));
984 assert!(
985 client
986 .create_dir(dir_path.as_path(), UnixPex::from(0o775))
987 .is_ok()
988 );
989 let mut file_path = dir_path.clone();
991 file_path.push(Path::new("a.txt"));
992 let file_data = "test data\n";
993 let reader = Cursor::new(file_data.as_bytes());
994 assert!(
995 client
996 .create_file(file_path.as_path(), &Metadata::default(), Box::new(reader))
997 .is_ok()
998 );
999 assert!(client.remove_dir(dir_path.as_path()).is_err());
1001 });
1002 }
1003
1004 #[test]
1005 fn should_remove_file() {
1006 with_client(|client| {
1007 let p = Path::new("a.txt");
1009 let file_data = "test data\n";
1010 let reader = Cursor::new(file_data.as_bytes());
1011 assert!(
1012 client
1013 .create_file(p, &Metadata::default(), Box::new(reader))
1014 .is_ok()
1015 );
1016 assert!(client.remove_file(p).is_ok());
1017 });
1018 }
1019
1020 #[test]
1021 fn should_not_setstat_file() {
1022 with_client(|client| {
1023 let p = Path::new("a.sh");
1025 assert!(
1026 client
1027 .setstat(
1028 p,
1029 Metadata {
1030 accessed: None,
1031 created: None,
1032 gid: Some(1),
1033 file_type: FileType::File,
1034 mode: Some(UnixPex::from(0o755)),
1035 modified: None,
1036 size: 7,
1037 symlink: None,
1038 uid: Some(1),
1039 }
1040 )
1041 .is_err()
1042 );
1043 });
1044 }
1045
1046 #[test]
1047 fn should_stat_file() {
1048 with_client(|client| {
1049 let p = Path::new("a.sh");
1051 let file_data = "echo 5\n";
1052 let reader = Cursor::new(file_data.as_bytes());
1053 assert!(
1054 client
1055 .create_file(p, &Metadata::default(), Box::new(reader))
1056 .is_ok()
1057 );
1058 let entry = client.stat(p).ok().unwrap();
1059 assert_eq!(entry.name(), "a.sh");
1060 let mut expected_path = client.pwd().ok().unwrap();
1061 expected_path.push("a.sh");
1062 assert_eq!(entry.path(), expected_path.as_path());
1063 let meta = entry.metadata();
1064 assert_eq!(meta.mode.unwrap(), UnixPex::from(0o644));
1065 assert_eq!(meta.size, 7);
1066 });
1067 }
1068
1069 #[test]
1070 fn should_stat_root() {
1071 with_client(|client| {
1072 let p = Path::new("/");
1074 let entry = client.stat(p).ok().unwrap();
1075 assert_eq!(entry.name(), "/");
1076 assert_eq!(entry.path(), Path::new("/"));
1077 assert!(entry.is_dir());
1078 });
1079 }
1080
1081 #[test]
1082 fn should_not_stat_file() {
1083 with_client(|client| {
1084 let p = Path::new("a.sh");
1086 assert!(client.stat(p).is_err());
1087 });
1088 }
1089
1090 #[test]
1091 fn should_not_make_symlink() {
1092 with_client(|client| {
1093 let p = Path::new("a.sh");
1095 let symlink = Path::new("b.sh");
1096 assert!(client.symlink(symlink, p).is_err());
1097 });
1098 }
1099
1100 #[test]
1101 fn should_return_not_connected_error() {
1102 let mut client = FtpFs::new("127.0.0.1", 21);
1103 assert!(client.change_dir(Path::new("/tmp")).is_err());
1104 assert!(
1105 client
1106 .copy(Path::new("/nowhere"), PathBuf::from("/culonia").as_path())
1107 .is_err()
1108 );
1109 assert!(client.exec("echo 5").is_err());
1110 assert!(client.disconnect().is_err());
1111 assert!(client.symlink(Path::new("/a"), Path::new("/b")).is_err());
1112 assert!(client.list_dir(Path::new("/tmp")).is_err());
1113 assert!(
1114 client
1115 .create_dir(Path::new("/tmp"), UnixPex::from(0o755))
1116 .is_err()
1117 );
1118 assert!(client.pwd().is_err());
1119 assert!(client.remove_dir_all(Path::new("/nowhere")).is_err());
1120 assert!(
1121 client
1122 .mov(Path::new("/nowhere"), Path::new("/culonia"))
1123 .is_err()
1124 );
1125 assert!(client.stat(Path::new("/tmp")).is_err());
1126 assert!(
1127 client
1128 .setstat(Path::new("/tmp"), Metadata::default())
1129 .is_err()
1130 );
1131 assert!(client.open(Path::new("/tmp/pippo.txt")).is_err());
1132 assert!(
1133 client
1134 .create(Path::new("/tmp/pippo.txt"), &Metadata::default())
1135 .is_err()
1136 );
1137 assert!(
1138 client
1139 .append(Path::new("/tmp/pippo.txt"), &Metadata::default())
1140 .is_err()
1141 );
1142 }
1143
1144 fn is_send<T: Send>(_send: T) {}
1145
1146 fn is_sync<T: Sync>(_sync: T) {}
1147
1148 #[test]
1149 fn test_should_be_sync() {
1150 let client = FtpFs::new("127.0.0.1", 10021)
1151 .username("test")
1152 .password("test");
1153
1154 is_sync(client);
1155 }
1156
1157 #[test]
1158 fn test_should_be_send() {
1159 let client = FtpFs::new("127.0.0.1", 10021)
1160 .username("test")
1161 .password("test");
1162
1163 is_send(client);
1164 }
1165
1166 fn generate_tempdir() -> String {
1169 use rand::{Rng, distr::Alphanumeric, rng};
1170 let mut rng = rng();
1171 let name: String = std::iter::repeat(())
1172 .map(|()| rng.sample(Alphanumeric))
1173 .map(char::from)
1174 .take(8)
1175 .collect();
1176 format!("temp_{}", name)
1177 }
1178
1179 fn with_client<F>(f: F)
1180 where
1181 F: FnOnce(&mut FtpFs),
1182 {
1183 crate::log_init();
1184 let container = Arc::new(SyncPureFtpRunner::start());
1185 let port = container.get_ftp_port();
1186
1187 let mut stream: FtpFs = setup_client("localhost", port, &container);
1189
1190 f(&mut stream);
1191 finalize_client(stream);
1192
1193 drop(container);
1194 }
1195
1196 fn setup_client(hostname: &str, port: u16, container: &Arc<SyncPureFtpRunner>) -> FtpFs {
1197 let container_t = container.clone();
1198
1199 let mut client = FtpFs::new(hostname, port)
1200 .username("test")
1201 .password("test")
1202 .passive_stream_builder(move |addr| {
1203 let mut addr = addr.clone();
1204 let port = addr.port();
1205 let mapped = container_t.get_mapped_port(port);
1206
1207 addr.set_port(mapped);
1208
1209 info!("mapped port {port} to {mapped} for PASV");
1210
1211 TcpStream::connect(addr).map_err(FtpError::ConnectionError)
1213 });
1214
1215 assert!(client.connect().is_ok());
1216 let tempdir = PathBuf::from(generate_tempdir());
1218 assert!(client.create_dir(&tempdir, UnixPex::from(0o755)).is_ok());
1219 assert!(client.change_dir(&tempdir).is_ok());
1221
1222 client
1223 }
1224
1225 fn finalize_client(mut client: FtpFs) {
1226 let wrkdir = client.pwd().ok().unwrap();
1228 assert!(client.remove_dir_all(wrkdir.as_path()).is_ok());
1230 assert!(client.disconnect().is_ok());
1231 }
1232}