remotefs_ftp/
client.rs

1//! # Ftp
2//!
3//! ftp client for remotefs
4
5use 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
36/// A function that creates a new stream for the data connection in passive mode.
37///
38/// It takes a [`SocketAddr`] and returns a [`TcpStream`].
39pub type PassiveStreamBuilder = dyn Fn(SocketAddr) -> FtpResult<TcpStream> + Send + Sync;
40
41/// Ftp file system client
42pub struct FtpFs {
43    /// Client
44    stream: Option<FtpStream>,
45    // -- options
46    hostname: String,
47    port: u16,
48    /// Username to login as; default: `anonymous`
49    username: String,
50    password: Option<String>,
51    /// passive stream builder
52    passive_stream_builder: Option<Box<PassiveStreamBuilder>>,
53    /// Client mode; default: `Mode::Passive`
54    mode: Mode,
55    #[cfg(any(feature = "native-tls", feature = "rustls"))]
56    /// use FTPS; default: `false`
57    secure: bool,
58    #[cfg(feature = "native-tls")]
59    /// Accept invalid certificates when building TLS connector. (Applies only if `secure`). Default: `false`
60    accept_invalid_certs: bool,
61    #[cfg(feature = "native-tls")]
62    /// Accept invalid hostnames when building TLS connector. (Applies only if `secure`). Default: `false`
63    accept_invalid_hostnames: bool,
64}
65
66impl FtpFs {
67    /// Instantiates a new `FtpFs`
68    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    // -- constructors
87
88    /// Set username
89    pub fn username<S: AsRef<str>>(mut self, username: S) -> Self {
90        self.username = username.as_ref().to_string();
91        self
92    }
93
94    /// Set password
95    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    /// Set active mode for client
101    pub fn active_mode(mut self) -> Self {
102        self.mode = Mode::Active;
103        self
104    }
105
106    /// Set passive mode for client
107    pub fn passive_mode(mut self) -> Self {
108        self.mode = Mode::Passive;
109        self
110    }
111
112    #[cfg(feature = "native-tls")]
113    /// enable FTPS and configure options
114    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    /// Set a custom [`PassiveStreamBuilder`] for passive mode.
128    ///
129    /// The stream builder is a function that takes a `SocketAddr` and returns a `TcpStream` and it's used
130    /// to create the [`TcpStream`] for the data connection in passive mode.
131    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    // -- as_ref
140
141    /// Get reference to inner stream
142    pub fn stream(&mut self) -> Option<&mut FtpStream> {
143        self.stream.as_mut()
144    }
145
146    // -- private
147
148    /// Parse all lines of LIST command output and instantiates a vector of `File` from it.
149    /// This function also converts from `suppaftp::list::File` to `File`
150    fn parse_list_lines(&mut self, path: &Path, lines: Vec<String>) -> Vec<File> {
151        // Iter and collect
152        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    /// Returns unix pex from ftp file pex
185    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    /// Fix provided path; on Windows fixes the backslashes, converting them to slashes
206    /// While on POSIX does nothing
207    #[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 provided, set passive stream builder
268        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        // If secure, connect TLS
274        #[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        // Login
293        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        // Resolve and absolutize path
374        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                // Return root
381                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        // Get target
391        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            // Create file
632            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            // Verify size
643            assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
644            // Append to file
645            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            // Create file
662            let p = Path::new("/tmp/aaaaaaa/hbbbbb/a.txt");
663            // Append to file
664            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            // create directory
705            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            // create directory
717            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            // create directory
737            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            // Create file
752            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            // Verify size
763            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            // Create file
771            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            // Create file
786            assert!(client.exec("echo 5").is_err());
787        });
788    }
789
790    #[test]
791    fn should_tell_whether_file_exists() {
792        with_client(|client| {
793            // Create file
794            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            // Verify size
803            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            // Create file
816            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            // Verify size
826            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            // Create file
848            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            // Verify size
857            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            // Create file
868            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            // Verify size
877            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            // Create file
891            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            // Verify size
900            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            // Verify size
909            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            // Create dir
929            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            // Create file
937            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            // Remove dir
947            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            // Remove dir
955            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            // Create dir
967            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            // Create dir
982            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            // Create file
990            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            // Remove dir
1000            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            // Create file
1008            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            // Create file
1024            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            // Create file
1050            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            // Create file
1073            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            // Create file
1085            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            // Create file
1094            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    // -- test utils
1167
1168    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        // init stream with mapper
1188        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                // open stream to this address instead
1212                TcpStream::connect(addr).map_err(FtpError::ConnectionError)
1213            });
1214
1215        assert!(client.connect().is_ok());
1216        // Create wrkdir
1217        let tempdir = PathBuf::from(generate_tempdir());
1218        assert!(client.create_dir(&tempdir, UnixPex::from(0o755)).is_ok());
1219        // Change directory
1220        assert!(client.change_dir(&tempdir).is_ok());
1221
1222        client
1223    }
1224
1225    fn finalize_client(mut client: FtpFs) {
1226        // Get working directory
1227        let wrkdir = client.pwd().ok().unwrap();
1228        // Remove directory
1229        assert!(client.remove_dir_all(wrkdir.as_path()).is_ok());
1230        assert!(client.disconnect().is_ok());
1231    }
1232}