Skip to main content

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