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}