remotefs_ssh/ssh/
sftp.rs

1//! ## SFTP
2//!
3//! Sftp remote fs implementation
4
5use std::io::{Read, Write};
6use std::path::{Path, PathBuf};
7
8use remotefs::File;
9use remotefs::fs::{
10    Metadata, ReadStream, RemoteError, RemoteErrorType, RemoteFs, RemoteResult, UnixPex, Welcome,
11    WriteStream,
12};
13
14use super::SshOpts;
15use crate::SshSession;
16use crate::ssh::backend::{Sftp as _, WriteMode};
17use crate::utils::path as path_utils;
18
19/// Sftp "filesystem" client
20pub struct SftpFs<S>
21where
22    S: SshSession,
23{
24    session: Option<S>,
25    sftp: Option<S::Sftp>,
26    wrkdir: PathBuf,
27    opts: SshOpts,
28}
29
30#[cfg(feature = "libssh2")]
31#[cfg_attr(docsrs, doc(cfg(feature = "libssh2")))]
32impl SftpFs<super::backend::LibSsh2Session> {
33    /// Constructs a new [`SftpFs`] instance with the `libssh2` backend.
34    pub fn libssh2(opts: SshOpts) -> Self {
35        Self {
36            session: None,
37            sftp: None,
38            wrkdir: PathBuf::from("/"),
39            opts,
40        }
41    }
42}
43
44#[cfg(feature = "libssh")]
45#[cfg_attr(docsrs, doc(cfg(feature = "libssh")))]
46impl SftpFs<super::backend::LibSshSession> {
47    /// Constructs a new [`SftpFs`] instance with the `libssh` backend.
48    pub fn libssh(opts: SshOpts) -> Self {
49        Self {
50            session: None,
51            sftp: None,
52            wrkdir: PathBuf::from("/"),
53            opts,
54        }
55    }
56}
57
58impl<S> SftpFs<S>
59where
60    S: SshSession,
61{
62    /// Get a reference to current `session` value.
63    pub fn session(&mut self) -> Option<&mut S> {
64        self.session.as_mut()
65    }
66
67    /// Get a reference to current `sftp` value.
68    pub fn sftp(&mut self) -> Option<&mut S::Sftp> {
69        self.sftp.as_mut()
70    }
71
72    // -- private
73
74    /// Check connection status
75    fn check_connection(&mut self) -> RemoteResult<()> {
76        if self.is_connected() {
77            Ok(())
78        } else {
79            Err(RemoteError::new(RemoteErrorType::NotConnected))
80        }
81    }
82}
83
84impl<S> RemoteFs for SftpFs<S>
85where
86    S: SshSession,
87{
88    fn connect(&mut self) -> RemoteResult<Welcome> {
89        debug!("Initializing SFTP connection...");
90        let mut session = S::connect(&self.opts)?;
91        // Get working directory
92        debug!("Getting working directory...");
93        self.wrkdir = session
94            .cmd("pwd")
95            .map(|(_rc, output)| PathBuf::from(output.as_str().trim()))?;
96        // Get Sftp client
97        debug!("Getting SFTP client...");
98        let sftp = match session.sftp() {
99            Ok(s) => s,
100            Err(err) => {
101                error!("Could not get sftp client: {err}");
102                return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
103            }
104        };
105        self.session = Some(session);
106        self.sftp = Some(sftp);
107        let banner = self.session.as_ref().unwrap().banner()?;
108        debug!(
109            "Connection established: '{}'; working directory {}",
110            banner.as_deref().unwrap_or(""),
111            self.wrkdir.display()
112        );
113        Ok(Welcome::default().banner(banner))
114    }
115
116    fn disconnect(&mut self) -> RemoteResult<()> {
117        debug!("Disconnecting from remote...");
118        if let Some(session) = self.session.as_ref() {
119            // First free sftp
120            self.sftp = None;
121            // Disconnect (greet server with 'Mandi' as they do in Friuli)
122            match session.disconnect() {
123                Ok(_) => {
124                    // Set session and sftp to none
125                    self.session = None;
126                    Ok(())
127                }
128                Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err)),
129            }
130        } else {
131            Err(RemoteError::new(RemoteErrorType::NotConnected))
132        }
133    }
134
135    fn is_connected(&mut self) -> bool {
136        self.session
137            .as_ref()
138            .map(|x| x.authenticated().unwrap_or_default())
139            .unwrap_or_default()
140    }
141
142    fn pwd(&mut self) -> RemoteResult<PathBuf> {
143        self.check_connection()?;
144        Ok(self.wrkdir.clone())
145    }
146
147    fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
148        self.check_connection()?;
149        let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
150        // Stat path to check if it exists. If it is a file, return error
151        match self.stat(dir.as_path()) {
152            Err(err) => Err(err),
153            Ok(file) if file.is_dir() => {
154                self.wrkdir = dir;
155                debug!("Changed working directory to {}", self.wrkdir.display());
156                Ok(self.wrkdir.clone())
157            }
158            Ok(_) => Err(RemoteError::new_ex(
159                RemoteErrorType::BadFile,
160                "expected directory, got file",
161            )),
162        }
163    }
164
165    fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
166        if let Some(sftp) = self.sftp.as_ref() {
167            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
168            debug!("Reading directory content of {}", path.display());
169            match sftp.readdir(path.as_path()) {
170                Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
171                Ok(files) => Ok(files),
172            }
173        } else {
174            Err(RemoteError::new(RemoteErrorType::NotConnected))
175        }
176    }
177
178    fn stat(&mut self, path: &Path) -> RemoteResult<File> {
179        if let Some(sftp) = self.sftp.as_ref() {
180            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
181            debug!("Collecting metadata for {}", path.display());
182            sftp.stat(path.as_path()).map_err(|e| {
183                error!("Stat failed: {e}");
184                RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, e)
185            })
186        } else {
187            Err(RemoteError::new(RemoteErrorType::NotConnected))
188        }
189    }
190
191    fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
192        if let Some(sftp) = self.sftp.as_ref() {
193            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
194            debug!("Setting metadata for {}", path.display());
195            sftp.setstat(path.as_path(), metadata)
196                .map(|_| ())
197                .map_err(|e| {
198                    error!("Setstat failed: {e}");
199                    RemoteError::new_ex(RemoteErrorType::StatFailed, e)
200                })
201        } else {
202            Err(RemoteError::new(RemoteErrorType::NotConnected))
203        }
204    }
205
206    fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
207        match self.stat(path) {
208            Ok(_) => Ok(true),
209            Err(RemoteError {
210                kind: RemoteErrorType::NoSuchFileOrDirectory,
211                ..
212            }) => Ok(false),
213            Err(err) => Err(err),
214        }
215    }
216
217    fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
218        if let Some(sftp) = self.sftp.as_ref() {
219            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
220            debug!("Remove file {}", path.display());
221            sftp.unlink(path.as_path()).map_err(|e| {
222                error!("Remove failed: {e}");
223                RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
224            })
225        } else {
226            Err(RemoteError::new(RemoteErrorType::NotConnected))
227        }
228    }
229
230    fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
231        if let Some(sftp) = self.sftp.as_ref() {
232            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
233            debug!("Remove dir {}", path.display());
234            sftp.rmdir(path.as_path()).map_err(|e| {
235                error!("Remove failed: {e}");
236                RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
237            })
238        } else {
239            Err(RemoteError::new(RemoteErrorType::NotConnected))
240        }
241    }
242
243    fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
244        self.check_connection()?;
245        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
246        if !self.exists(path.as_path()).ok().unwrap_or(false) {
247            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
248        }
249        debug!("Removing directory {} recursively", path.display());
250        match self
251            .session
252            .as_mut()
253            .unwrap()
254            .cmd(format!("rm -rf \"{}\"", path.display()))
255        {
256            Ok((0, _)) => Ok(()),
257            Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
258            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
259        }
260    }
261
262    fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
263        self.check_connection()?;
264        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
265        // Check if already exists
266        debug!(
267            "Creating directory {} (mode: {:o})",
268            path.display(),
269            u32::from(mode)
270        );
271        if self.exists(path.as_path())? {
272            error!("directory {} already exists", path.display());
273            return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
274        }
275        self.sftp
276            .as_ref()
277            .unwrap()
278            .mkdir(path.as_path(), u32::from(mode) as i32)
279            .map_err(|e| {
280                error!("Create dir failed: {e}");
281                RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
282            })
283    }
284
285    fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> {
286        self.check_connection()?;
287        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
288        // Check if already exists
289        debug!(
290            "Creating symlink at {} pointing to {}",
291            path.display(),
292            target.display()
293        );
294        if !self.exists(target)? {
295            error!("target {} doesn't exist", target.display());
296            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
297        }
298        self.sftp
299            .as_ref()
300            .unwrap()
301            .symlink(target, path.as_path())
302            .map_err(|e| {
303                error!("Symlink failed: {e}");
304                RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
305            })
306    }
307
308    fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
309        self.check_connection()?;
310        let src = path_utils::absolutize(self.wrkdir.as_path(), src);
311        // check if file exists
312        if !self.exists(src.as_path()).ok().unwrap_or(false) {
313            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
314        }
315        let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
316        debug!("Copying {} to {}", src.display(), dest.display());
317        // Run `cp -rf`
318        match self
319            .session
320            .as_mut()
321            .unwrap()
322            .cmd(format!("cp -rf \"{}\" \"{}\"", src.display(), dest.display()).as_str())
323        {
324            Ok((0, _)) => Ok(()),
325            Ok(_) => Err(RemoteError::new_ex(
326                // Could not copy file
327                RemoteErrorType::FileCreateDenied,
328                format!("\"{}\"", dest.display()),
329            )),
330            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
331        }
332    }
333
334    fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
335        self.check_connection()?;
336        let src = path_utils::absolutize(self.wrkdir.as_path(), src);
337        // check if file exists
338        if !self.exists(src.as_path()).ok().unwrap_or(false) {
339            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
340        }
341        let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
342        debug!("Moving {} to {}", src.display(), dest.display());
343        self.sftp
344            .as_ref()
345            .unwrap()
346            .rename(src.as_path(), dest.as_path())
347            .map_err(|e| {
348                error!("Move failed: {e}",);
349                RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
350            })
351    }
352
353    fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> {
354        self.check_connection()?;
355        debug!(r#"Executing command "{cmd}""#);
356
357        self.session
358            .as_mut()
359            .unwrap()
360            .cmd_at(cmd, self.wrkdir.as_path())
361    }
362
363    fn append(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
364        if let Some(sftp) = self.sftp.as_ref() {
365            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
366            debug!("Opening file at {} for appending", path.display());
367            let mode = metadata.mode.map(|x| u32::from(x) as i32).unwrap_or(0o644);
368            sftp.open_write(path.as_path(), WriteMode::Append, mode)
369                .map_err(|e| {
370                    error!("Append failed: {e}",);
371                    RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
372                })
373        } else {
374            Err(RemoteError::new(RemoteErrorType::NotConnected))
375        }
376    }
377
378    fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
379        if let Some(sftp) = self.sftp.as_ref() {
380            let path = path_utils::absolutize(self.wrkdir.as_path(), path);
381            debug!("Creating file at {}", path.display());
382            let mode = metadata.mode.map(|x| u32::from(x) as i32).unwrap_or(0o644);
383            sftp.open_write(path.as_path(), WriteMode::Truncate, mode)
384                .map_err(|e| {
385                    error!("Create failed: {e}",);
386                    RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
387                })
388        } else {
389            Err(RemoteError::new(RemoteErrorType::NotConnected))
390        }
391    }
392
393    fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
394        self.check_connection()?;
395        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
396        // check if file exists
397        if !self.exists(path.as_path()).ok().unwrap_or(false) {
398            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
399        }
400        debug!("Opening file at {}", path.display());
401        self.sftp
402            .as_ref()
403            .unwrap()
404            .open_read(path.as_path())
405            .map_err(|e| {
406                error!("Open failed: {e}",);
407                RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
408            })
409    }
410
411    // -- override (std::io::copy is VERY slow on SFTP <https://github.com/remotefs-rs/remotefs-rs/issues/6>)
412
413    fn append_file(
414        &mut self,
415        path: &Path,
416        metadata: &Metadata,
417        mut reader: Box<dyn Read + Send>,
418    ) -> RemoteResult<u64> {
419        if self.is_connected() {
420            let mut stream = self.append(path, metadata)?;
421            trace!("Opened remote file");
422            let mut bytes: usize = 0;
423            let transfer_size = metadata.size as usize;
424            let mut buffer: [u8; 65535] = [0; 65535];
425            while bytes < transfer_size {
426                let bytes_read = reader.read(&mut buffer).map_err(|e| {
427                    error!("Failed to read from file: {e}",);
428                    RemoteError::new_ex(RemoteErrorType::IoError, e)
429                })?;
430                let mut delta = 0;
431                while delta < bytes_read {
432                    delta += stream.write(&buffer[delta..bytes_read]).map_err(|e| {
433                        error!("Failed to write to stream: {e}",);
434                        RemoteError::new_ex(RemoteErrorType::IoError, e)
435                    })?;
436                }
437                bytes += bytes_read;
438            }
439            self.on_written(stream)?;
440            trace!("Written {bytes} bytes to destination",);
441            Ok(bytes as u64)
442        } else {
443            Err(RemoteError::new(RemoteErrorType::NotConnected))
444        }
445    }
446
447    fn create_file(
448        &mut self,
449        path: &Path,
450        metadata: &Metadata,
451        mut reader: Box<dyn std::io::Read + Send>,
452    ) -> RemoteResult<u64> {
453        if self.is_connected() {
454            let mut stream = self.create(path, metadata)?;
455            trace!("Opened remote file");
456            let mut bytes: usize = 0;
457            let transfer_size = metadata.size as usize;
458            let mut buffer: [u8; 65535] = [0; 65535];
459            while bytes < transfer_size {
460                let bytes_read = reader.read(&mut buffer).map_err(|e| {
461                    error!("Failed to read from file: {e}",);
462                    RemoteError::new_ex(RemoteErrorType::IoError, e)
463                })?;
464                let mut delta = 0;
465                while delta < bytes_read {
466                    delta += stream.write(&buffer[delta..bytes_read]).map_err(|e| {
467                        error!("Failed to write to stream: {e}",);
468                        RemoteError::new_ex(RemoteErrorType::IoError, e)
469                    })?;
470                }
471                bytes += bytes_read;
472            }
473            stream.flush().map_err(|e| {
474                error!("Failed to flush stream: {e}");
475                RemoteError::new_ex(RemoteErrorType::IoError, e)
476            })?;
477            self.on_written(stream)?;
478            trace!("Written {bytes} bytes to destination",);
479            Ok(bytes as u64)
480        } else {
481            Err(RemoteError::new(RemoteErrorType::NotConnected))
482        }
483    }
484
485    fn open_file(&mut self, src: &Path, mut dest: Box<dyn Write + Send>) -> RemoteResult<u64> {
486        if self.is_connected() {
487            let transfer_size = self.stat(src)?.metadata().size as usize;
488            let mut stream = self.open(src)?;
489            trace!("File opened");
490            let mut bytes: usize = 0;
491            let mut buffer: [u8; 65535] = [0; 65535];
492            while bytes < transfer_size {
493                let bytes_read = stream.read(&mut buffer).map_err(|e| {
494                    error!("Failed to read from stream: {e}");
495                    RemoteError::new_ex(RemoteErrorType::IoError, e)
496                })?;
497                let mut delta = 0;
498                while delta < bytes_read {
499                    delta += dest.write(&buffer[delta..bytes_read]).map_err(|e| {
500                        error!("Failed to write to file: {e}",);
501                        RemoteError::new_ex(RemoteErrorType::IoError, e)
502                    })?;
503                }
504                bytes += bytes_read;
505            }
506            self.on_read(stream)?;
507            trace!("Copied {bytes} bytes to destination",);
508            Ok(bytes as u64)
509        } else {
510            Err(RemoteError::new(RemoteErrorType::NotConnected))
511        }
512    }
513}
514
515#[cfg(test)]
516mod tests;