remotefs/fs/
sync.rs

1use std::io;
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4
5#[cfg(feature = "find")]
6use wildmatch::WildMatch;
7
8use super::{
9    File, Metadata, ReadStream, RemoteError, RemoteErrorType, UnixPex, Welcome, WriteStream,
10};
11use crate::RemoteResult;
12
13/// Defines the methods which must be implemented in order to setup a Remote file system
14pub trait RemoteFs {
15    /// Connect to the remote server and authenticate.
16    /// Can return banner / welcome message on success.
17    /// If client has already established connection, then [`RemoteErrorType::AlreadyConnected`] error is returned.
18    fn connect(&mut self) -> RemoteResult<Welcome>;
19
20    /// Disconnect from the remote server
21    fn disconnect(&mut self) -> RemoteResult<()>;
22
23    /// Gets whether the client is connected to remote
24    fn is_connected(&mut self) -> bool;
25
26    /// Get working directory
27    fn pwd(&mut self) -> RemoteResult<PathBuf>;
28
29    /// Change working directory.
30    /// Returns the realpath of new directory
31    fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf>;
32
33    /// List directory entries at specified `path`
34    fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>>;
35
36    /// Stat file at specified `path` and return [`File`]
37    fn stat(&mut self, path: &Path) -> RemoteResult<File>;
38
39    /// Set metadata for file at specified `path`
40    fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()>;
41
42    /// Returns whether file at specified `path` exists.
43    fn exists(&mut self, path: &Path) -> RemoteResult<bool>;
44
45    /// Remove file at specified `path`.
46    /// Fails if is not a file or doesn't exist
47    fn remove_file(&mut self, path: &Path) -> RemoteResult<()>;
48
49    /// Remove directory at specified `path`
50    /// Directory is removed only if empty
51    fn remove_dir(&mut self, path: &Path) -> RemoteResult<()>;
52
53    /// Removes a directory at this path, after removing all its contents. **Use carefully!**
54    ///
55    /// If path is a [`crate::fs::FileType::File`], file is removed anyway, as it was a file (after all, directories are files!)
56    ///
57    /// This function does not follow symbolic links and it will simply remove the symbolic link itself.
58    ///
59    /// ### Default implementation
60    ///
61    /// By default this method will combine [`RemoteFs::remove_dir`] and [`RemoteFs::remove_file`] to remove all the content.
62    /// Implement this method when there is a faster way to achieve this
63    fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
64        if self.is_connected() {
65            let path = crate::utils::path::absolutize(&self.pwd()?, path);
66            debug!("Removing {}...", path.display());
67            let entry = self.stat(path.as_path())?;
68            if entry.is_dir() {
69                // list dir
70                debug!(
71                    "{} is a directory; removing all directory entries",
72                    entry.name()
73                );
74                let directory_content = self.list_dir(entry.path())?;
75                for entry in directory_content.iter() {
76                    self.remove_dir_all(entry.path())?;
77                }
78                trace!(
79                    "Removed all files in {}; removing directory",
80                    entry.path().display()
81                );
82                self.remove_dir(entry.path())
83            } else {
84                self.remove_file(entry.path())
85            }
86        } else {
87            Err(RemoteError::new(RemoteErrorType::NotConnected))
88        }
89    }
90
91    /// Create a directory at `path` with specified mode.
92    ///
93    /// If the directory already exists, it **MUST** return [`RemoteErrorType::DirectoryAlreadyExists`]
94    fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()>;
95
96    /// Create a symlink at `path` pointing at `target`
97    fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()>;
98
99    /// Copy `src` to `dest`
100    fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()>;
101
102    /// move file/directory from `src` to `dest`
103    fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()>;
104
105    /// Execute a command on remote host if supported by host.
106    /// Returns command exit code and output (stdout)
107    fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)>;
108
109    /// Open file at `path` for appending data.
110    /// If the file doesn't exist, the file is created.
111    ///
112    /// ### ⚠️ Warning
113    ///
114    /// metadata should be the same of the local file.
115    /// In some protocols, such as `scp` the `size` field is used to define the transfer size (required by the protocol)
116    fn append(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream>;
117
118    /// Create file at path for write.
119    /// If the file already exists, its content will be overwritten
120    ///
121    /// ### ⚠️ Warning
122    ///
123    /// metadata should be the same of the local file.
124    /// In some protocols, such as `scp` the `size` field is used to define the transfer size (required by the protocol)
125    fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream>;
126
127    /// Open file at specified path for read.
128    fn open(&mut self, path: &Path) -> RemoteResult<ReadStream>;
129
130    /// Finalize [`RemoteFs::create`] and [`RemoteFs::append`] methods.
131    /// This method must be implemented only if necessary; in case you don't need it, just return [`Ok`]
132    /// The purpose of this method is to finalize the connection with the peer when writing data.
133    /// This is necessary for some protocols such as FTP.
134    /// You must call this method each time you want to finalize the write of the remote file.
135    ///
136    /// ### Default implementation
137    ///
138    /// By default this function returns already [`Ok`]
139    fn on_written(&mut self, _writable: WriteStream) -> RemoteResult<()> {
140        Ok(())
141    }
142
143    /// Finalize [`RemoteFs::open`] method.
144    /// This method must be implemented only if necessary; in case you don't need it, just return [`Ok`]
145    /// The purpose of this method is to finalize the connection with the peer when reading data.
146    /// This might be necessary for some protocols.
147    /// You must call this method each time you want to finalize the read of the remote file.
148    ///
149    /// ### Default implementation
150    ///
151    /// By default this function returns already [`Ok`]
152    fn on_read(&mut self, _readable: ReadStream) -> RemoteResult<()> {
153        Ok(())
154    }
155
156    /// Blocking implementation of [`RemoteFs::append`]
157    /// This method **SHOULD** be implemented **ONLY** when streams are not supported by the current file transfer.
158    /// The developer using the client should FIRST try with `create` followed by `on_written`
159    /// If the function returns error of kind [`RemoteErrorType::UnsupportedFeature`], then he should call this function.
160    /// In case of success, returns the amount of bytes written to the remote file
161    ///
162    /// ### Default implementation
163    ///
164    /// By default this function uses the streams function to copy content from reader to writer
165    fn append_file(
166        &mut self,
167        path: &Path,
168        metadata: &Metadata,
169        mut reader: Box<dyn Read + Send>,
170    ) -> RemoteResult<u64> {
171        if self.is_connected() {
172            trace!("Opened remote file");
173            let mut stream = self.append(path, metadata)?;
174            let sz = io::copy(&mut reader, &mut stream)
175                .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e.to_string()))?;
176            self.on_written(stream)?;
177            trace!("Written {} bytes to destination", sz);
178            Ok(sz)
179        } else {
180            Err(RemoteError::new(RemoteErrorType::NotConnected))
181        }
182    }
183
184    /// Blocking implementation of [`RemoteFs::create`]
185    /// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
186    /// The developer using the client should FIRST try with `create` followed by `on_written`
187    /// If the function returns error of kind [`RemoteErrorType::UnsupportedFeature`], then he should call this function.
188    /// In case of success, returns the amount of bytes written to the remote file
189    ///
190    /// ### Default implementation
191    ///
192    /// By default this function uses the streams function to copy content from reader to writer
193    fn create_file(
194        &mut self,
195        path: &Path,
196        metadata: &Metadata,
197        mut reader: Box<dyn Read + Send>,
198    ) -> RemoteResult<u64> {
199        if self.is_connected() {
200            let mut stream = self.create(path, metadata)?;
201            trace!("Opened remote file");
202            let sz = io::copy(&mut reader, &mut stream)
203                .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e.to_string()))?;
204            self.on_written(stream)?;
205            trace!("Written {} bytes to destination", sz);
206            Ok(sz)
207        } else {
208            Err(RemoteError::new(RemoteErrorType::NotConnected))
209        }
210    }
211
212    /// Blocking implementation of [`RemoteFs::open`]
213    /// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
214    /// (since it would work thanks to the default implementation)
215    /// The developer using the client should FIRST try with [`RemoteFs::open`] followed by [`RemoteFs::on_read`]
216    /// If the function returns error of kind [`RemoteErrorType::UnsupportedFeature`], then he should call this function.
217    /// In case of success, returns the amount of bytes written to the local stream
218    ///
219    /// ### Default implementation
220    ///
221    /// By default this function uses the streams function to copy content from reader to writer
222    fn open_file(&mut self, src: &Path, mut dest: Box<dyn Write + Send>) -> RemoteResult<u64> {
223        if self.is_connected() {
224            let mut stream = self.open(src)?;
225            trace!("File opened");
226            let sz = io::copy(&mut stream, &mut dest)
227                .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e.to_string()))?;
228            self.on_read(stream)?;
229            trace!("Copied {} bytes to destination", sz);
230            Ok(sz)
231        } else {
232            Err(RemoteError::new(RemoteErrorType::NotConnected))
233        }
234    }
235
236    /// Find files from current directory (in all subdirectories) whose name matches the provided search
237    /// Search supports wildcards ('?', '*')
238    #[cfg(feature = "find")]
239    fn find(&mut self, search: &str) -> RemoteResult<Vec<File>> {
240        match self.is_connected() {
241            true => {
242                // Starting from current directory, iter dir
243                match self.pwd() {
244                    Ok(p) => self.iter_search(p.as_path(), &WildMatch::new(search)),
245                    Err(err) => Err(err),
246                }
247            }
248            false => Err(RemoteError::new(RemoteErrorType::NotConnected)),
249        }
250    }
251
252    /// Search recursively in `dir` for file matching the wildcard.
253    ///
254    /// ### ⚠️ Warning
255    ///
256    /// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so
257    /// NOTE: don't call this method from outside; consider it as private
258    #[cfg(feature = "find")]
259    fn iter_search(&mut self, dir: &Path, filter: &WildMatch) -> RemoteResult<Vec<File>> {
260        let mut drained: Vec<File> = Vec::new();
261        // Scan directory
262        match self.list_dir(dir) {
263            Ok(entries) => {
264                /* For each entry:
265                - if is dir: call iter_search with `dir`
266                    - push `iter_search` result to `drained`
267                - if is file: check if it matches `filter`
268                    - if it matches `filter`: push to to filter
269                */
270                for entry in entries.into_iter() {
271                    if entry.is_dir() {
272                        // If directory name, matches wildcard, push it to drained
273                        if filter.matches(entry.name().as_str()) {
274                            drained.push(entry.clone());
275                        }
276                        drained.append(&mut self.iter_search(entry.path(), filter)?);
277                    } else if filter.matches(entry.name().as_str()) {
278                        drained.push(entry);
279                    }
280                }
281                Ok(drained)
282            }
283            Err(err) => Err(err),
284        }
285    }
286}
287
288#[cfg(test)]
289mod test {
290
291    use super::*;
292    use crate::mock::MockRemoteFs;
293
294    #[test]
295    fn should_be_able_to_create_trait_object() {
296        let _: Box<dyn RemoteFs> = Box::new(MockRemoteFs {});
297    }
298}