remotefs_ssh/ssh/
scp.rs

1//! ## SCP
2//!
3//! Scp remote fs implementation
4
5use std::ops::Range;
6use std::path::{Path, PathBuf};
7use std::time::{Duration, SystemTime};
8
9use lazy_regex::{Lazy, Regex};
10use remotefs::File;
11use remotefs::fs::{
12    FileType, Metadata, ReadStream, RemoteError, RemoteErrorType, RemoteFs, RemoteResult, UnixPex,
13    UnixPexClass, Welcome, WriteStream,
14};
15
16use super::SshOpts;
17use crate::SshSession;
18use crate::utils::{fmt as fmt_utils, parser as parser_utils, path as path_utils};
19
20/// NOTE: about this damn regex <https://stackoverflow.com/questions/32480890/is-there-a-regex-to-parse-the-values-from-an-ftp-directory-listing>
21static LS_RE: Lazy<Regex> = lazy_regex!(
22    r#"^(?<sym_dir>[\-ld])(?<pex>[\-rwxsStT]{9})(?<sec_ctx>\.|\+|\@)?\s+(?<n_links>\d+)\s+(?<uid>.+)\s+(?<gid>.+)\s+(?<size>\d+)\s+(?<date_time>\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(?<name>.+)$"#
23);
24
25/// SCP "filesystem" client
26pub struct ScpFs<S>
27where
28    S: SshSession,
29{
30    session: Option<S>,
31    wrkdir: PathBuf,
32    opts: SshOpts,
33}
34
35#[cfg(feature = "libssh2")]
36#[cfg_attr(docsrs, doc(cfg(feature = "libssh2")))]
37impl ScpFs<super::backend::LibSsh2Session> {
38    /// Constructs a new [`ScpFs`] instance with the `libssh2` backend.
39    pub fn libssh2(opts: SshOpts) -> Self {
40        Self {
41            session: None,
42            wrkdir: PathBuf::from("/"),
43            opts,
44        }
45    }
46}
47
48#[cfg(feature = "libssh")]
49#[cfg_attr(docsrs, doc(cfg(feature = "libssh")))]
50impl ScpFs<super::backend::LibSshSession> {
51    /// Constructs a new [`ScpFs`] instance with the `libssh` backend.
52    pub fn libssh(opts: SshOpts) -> Self {
53        Self {
54            session: None,
55            wrkdir: PathBuf::from("/"),
56            opts,
57        }
58    }
59}
60
61impl<S> ScpFs<S>
62where
63    S: SshSession,
64{
65    /// Get a reference to current `session` value.
66    pub fn session(&mut self) -> Option<&mut S> {
67        self.session.as_mut()
68    }
69
70    // -- private
71
72    /// Check connection status
73    fn check_connection(&mut self) -> RemoteResult<()> {
74        if self.is_connected() {
75            Ok(())
76        } else {
77            Err(RemoteError::new(RemoteErrorType::NotConnected))
78        }
79    }
80
81    /// Parse a line of `ls -l` output and tokenize the output into a `FsFile`
82    fn parse_ls_output(&self, path: &Path, line: &str) -> Result<File, ()> {
83        // Prepare list regex
84        trace!("Parsing LS line: '{line}'");
85        // Apply regex to result
86        match LS_RE.captures(line) {
87            // String matches regex
88            Some(metadata) => {
89                // NOTE: metadata fmt: (regex, file_type, permissions, link_count, uid, gid, filesize, modified, filename)
90                // Expected 7 + 1 (8) values: + 1 cause regex is repeated at 0
91                if metadata.len() < 8 {
92                    return Err(());
93                }
94                // Collect metadata
95                // Get if is directory and if is symlink
96
97                let (is_dir, is_symlink): (bool, bool) = match &metadata["sym_dir"] {
98                    "-" => (false, false),
99                    "l" => (false, true),
100                    "d" => (true, false),
101                    _ => return Err(()), // Ignore special files
102                };
103                // Check string length (unix pex)
104                if metadata["pex"].len() < 9 {
105                    return Err(());
106                }
107
108                let pex = |range: Range<usize>| {
109                    let mut count: u8 = 0;
110                    for (i, c) in metadata["pex"][range].chars().enumerate() {
111                        match c {
112                            '-' => {}
113                            _ => {
114                                count += match i {
115                                    0 => 4,
116                                    1 => 2,
117                                    2 => 1,
118                                    _ => 0,
119                                }
120                            }
121                        }
122                    }
123                    count
124                };
125
126                // Get unix pex
127                let mode = UnixPex::new(
128                    UnixPexClass::from(pex(0..3)),
129                    UnixPexClass::from(pex(3..6)),
130                    UnixPexClass::from(pex(6..9)),
131                );
132
133                // Parse modified and convert to SystemTime
134                let modified: SystemTime = match parser_utils::parse_lstime(
135                    &metadata["date_time"],
136                    "%b %d %Y",
137                    "%b %d %H:%M",
138                ) {
139                    Ok(t) => t,
140                    Err(_) => SystemTime::UNIX_EPOCH,
141                };
142                // Get uid
143                let uid: Option<u32> = metadata["uid"].parse::<u32>().ok();
144                // Get gid
145                let gid: Option<u32> = metadata["gid"].parse::<u32>().ok();
146                // Get filesize
147                let size = metadata["size"].parse::<u64>().unwrap_or(0);
148                // Get link and name
149                let (file_name, symlink): (String, Option<PathBuf>) = match is_symlink {
150                    true => self.get_name_and_link(&metadata["name"]),
151                    false => (String::from(&metadata["name"]), None),
152                };
153                // Sanitize file name
154                let file_name = PathBuf::from(&file_name)
155                    .file_name()
156                    .map(|x| x.to_string_lossy().to_string())
157                    .unwrap_or(file_name);
158                // Check if file_name is '.' or '..'
159                if file_name.as_str() == "." || file_name.as_str() == ".." {
160                    return Err(());
161                }
162                // Re-check if is directory
163                let mut path: PathBuf = path.to_path_buf();
164                path.push(file_name.as_str());
165                // get file type
166                let file_type = if symlink.is_some() {
167                    FileType::Symlink
168                } else if is_dir {
169                    FileType::Directory
170                } else {
171                    FileType::File
172                };
173                // make metadata
174                let metadata = Metadata {
175                    accessed: None,
176                    created: None,
177                    file_type,
178                    gid,
179                    mode: Some(mode),
180                    modified: Some(modified),
181                    size,
182                    symlink,
183                    uid,
184                };
185                trace!(
186                    "Found entry at {} with metadata {:?}",
187                    path.display(),
188                    metadata
189                );
190                // Push to entries
191                Ok(File { path, metadata })
192            }
193            None => Err(()),
194        }
195    }
196
197    /// ### get_name_and_link
198    ///
199    /// Returns from a `ls -l` command output file name token, the name of the file and the symbolic link (if there is any)
200    fn get_name_and_link(&self, token: &str) -> (String, Option<PathBuf>) {
201        let tokens: Vec<&str> = token.split(" -> ").collect();
202        let filename: String = String::from(*tokens.first().unwrap());
203        let symlink: Option<PathBuf> = tokens.get(1).map(PathBuf::from);
204        (filename, symlink)
205    }
206
207    /// Execute setstat command and assert result is 0
208    fn assert_stat_command(&mut self, cmd: String) -> RemoteResult<()> {
209        match self.session.as_mut().unwrap().cmd(cmd) {
210            Ok((0, _)) => Ok(()),
211            Ok(_) => Err(RemoteError::new(RemoteErrorType::StatFailed)),
212            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
213        }
214    }
215
216    /// Returns whether file at `path` is a directory
217    fn is_directory(&mut self, path: &Path) -> RemoteResult<bool> {
218        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
219        match self
220            .session
221            .as_mut()
222            .unwrap()
223            .cmd(format!("test -d \"{}\"", path.display()))
224        {
225            Ok((0, _)) => Ok(true),
226            Ok(_) => Ok(false),
227            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
228        }
229    }
230}
231
232impl<S> RemoteFs for ScpFs<S>
233where
234    S: SshSession,
235{
236    fn connect(&mut self) -> RemoteResult<Welcome> {
237        debug!("Initializing SFTP connection...");
238        let mut session = S::connect(&self.opts)?;
239        // Get banner
240        let banner = session.banner()?;
241        debug!(
242            "Connection established: {}",
243            banner.as_deref().unwrap_or("")
244        );
245        // Get working directory
246        debug!("Getting working directory...");
247        self.wrkdir = session
248            .cmd("pwd")
249            .map(|(_rc, output)| PathBuf::from(output.as_str().trim()))?;
250        // Set session
251        self.session = Some(session);
252        info!(
253            "Connection established; working directory: {}",
254            self.wrkdir.display()
255        );
256        Ok(Welcome::default().banner(banner))
257    }
258
259    fn disconnect(&mut self) -> RemoteResult<()> {
260        debug!("Disconnecting from remote...");
261        if let Some(session) = self.session.as_ref() {
262            // Disconnect (greet server with 'Mandi' as they do in Friuli)
263            match session.disconnect() {
264                Ok(_) => {
265                    // Set session and sftp to none
266                    self.session = None;
267                    Ok(())
268                }
269                Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err)),
270            }
271        } else {
272            Err(RemoteError::new(RemoteErrorType::NotConnected))
273        }
274    }
275
276    fn is_connected(&mut self) -> bool {
277        self.session
278            .as_ref()
279            .map(|x| x.authenticated().unwrap_or_default())
280            .unwrap_or(false)
281    }
282
283    fn pwd(&mut self) -> RemoteResult<PathBuf> {
284        self.check_connection()?;
285        Ok(self.wrkdir.clone())
286    }
287
288    fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
289        self.check_connection()?;
290        let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
291        debug!("Changing working directory to {}", dir.display());
292        match self
293            .session
294            .as_mut()
295            .unwrap()
296            .cmd(format!("cd \"{}\"; echo $?; pwd", dir.display()))
297        {
298            Ok((rc, output)) => {
299                if rc != 0 {
300                    return Err(RemoteError::new_ex(
301                        RemoteErrorType::ProtocolError,
302                        format!("Failed to change directory: {}", output),
303                    ));
304                }
305                // Trim
306                let output: String = String::from(output.as_str().trim());
307                // Check if output starts with 0; should be 0{PWD}
308                match output.as_str().starts_with('0') {
309                    true => {
310                        // Set working directory
311                        self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
312                        debug!("Changed working directory to {}", self.wrkdir.display());
313                        Ok(self.wrkdir.clone())
314                    }
315                    false => Err(RemoteError::new_ex(
316                        // No such file or directory
317                        RemoteErrorType::NoSuchFileOrDirectory,
318                        format!("\"{}\"", dir.display()),
319                    )),
320                }
321            }
322            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
323        }
324    }
325
326    fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
327        self.check_connection()?;
328        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
329        debug!("Getting file entries in {}", path.display());
330        // check if exists
331        if !self.exists(path.as_path()).ok().unwrap_or(false) {
332            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
333        }
334        match self
335            .session
336            .as_mut()
337            .unwrap()
338            .cmd(format!("unset LANG; ls -la \"{}/\"", path.display()).as_str())
339        {
340            Ok((rc, output)) => {
341                if rc != 0 {
342                    return Err(RemoteError::new_ex(
343                        RemoteErrorType::ProtocolError,
344                        format!("Failed to list directory: {}", output),
345                    ));
346                }
347                // Split output by (\r)\n
348                let lines: Vec<&str> = output.as_str().lines().collect();
349                let mut entries: Vec<File> = Vec::with_capacity(lines.len());
350                for line in lines.iter() {
351                    // First line must always be ignored
352                    // Parse row, if ok push to entries
353                    if let Ok(entry) = self.parse_ls_output(path.as_path(), line) {
354                        entries.push(entry);
355                    }
356                }
357                debug!(
358                    "Found {} out of {} valid file entries",
359                    entries.len(),
360                    lines.len()
361                );
362                Ok(entries)
363            }
364            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
365        }
366    }
367
368    fn stat(&mut self, path: &Path) -> RemoteResult<File> {
369        self.check_connection()?;
370        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
371        debug!("Stat {}", path.display());
372        // make command; Directories require `-d` option
373        let cmd = match self.is_directory(path.as_path())? {
374            true => format!("ls -ld \"{}\"", path.display()),
375            false => format!("ls -l \"{}\"", path.display()),
376        };
377        match self.session.as_mut().unwrap().cmd(cmd.as_str()) {
378            Ok((rc, line)) => {
379                if rc != 0 {
380                    return Err(RemoteError::new_ex(
381                        RemoteErrorType::NoSuchFileOrDirectory,
382                        format!("Failed to stat file: {line}"),
383                    ));
384                }
385                // Parse ls line
386                let parent: PathBuf = match path.as_path().parent() {
387                    Some(p) => PathBuf::from(p),
388                    None => {
389                        return Err(RemoteError::new_ex(
390                            RemoteErrorType::StatFailed,
391                            "Path has no parent",
392                        ));
393                    }
394                };
395                match self.parse_ls_output(parent.as_path(), line.as_str().trim()) {
396                    Ok(entry) => Ok(entry),
397                    Err(_) => Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory)),
398                }
399            }
400            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
401        }
402    }
403
404    fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
405        self.check_connection()?;
406        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
407        match self
408            .session
409            .as_mut()
410            .unwrap()
411            .cmd(format!("test -e \"{}\"", path.display()))
412        {
413            Ok((0, _)) => Ok(true),
414            Ok(_) => Ok(false),
415            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
416        }
417    }
418
419    fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
420        self.check_connection()?;
421        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
422        debug!("Setting attributes for {}", path.display());
423        if !self.exists(path.as_path()).ok().unwrap_or(false) {
424            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
425        }
426        // set mode with chmod
427        if let Some(mode) = metadata.mode {
428            self.assert_stat_command(format!(
429                "chmod {:o} \"{}\"",
430                u32::from(mode),
431                path.display()
432            ))?;
433        }
434        if let Some(user) = metadata.uid {
435            self.assert_stat_command(format!(
436                "chown {}{} \"{}\"",
437                user,
438                metadata.gid.map(|x| format!(":{x}")).unwrap_or_default(),
439                path.display()
440            ))?;
441        }
442        // set times
443        if let Some(accessed) = metadata.accessed {
444            self.assert_stat_command(format!(
445                "touch -a -t {} \"{}\"",
446                fmt_utils::fmt_time_utc(accessed, "%Y%m%d%H%M.%S"),
447                path.display()
448            ))?;
449        }
450        if let Some(modified) = metadata.modified {
451            self.assert_stat_command(format!(
452                "touch -m -t {} \"{}\"",
453                fmt_utils::fmt_time_utc(modified, "%Y%m%d%H%M.%S"),
454                path.display()
455            ))?;
456        }
457        Ok(())
458    }
459
460    fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
461        self.check_connection()?;
462        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
463        if !self.exists(path.as_path()).ok().unwrap_or(false) {
464            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
465        }
466        debug!("Removing file {}", path.display());
467        match self
468            .session
469            .as_mut()
470            .unwrap()
471            .cmd(format!("rm -f \"{}\"", path.display()))
472        {
473            Ok((0, _)) => Ok(()),
474            Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
475            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
476        }
477    }
478
479    fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
480        self.check_connection()?;
481        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
482        if !self.exists(path.as_path()).ok().unwrap_or(false) {
483            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
484        }
485        debug!("Removing directory {}", path.display());
486        match self
487            .session
488            .as_mut()
489            .unwrap()
490            .cmd(format!("rmdir \"{}\"", path.display()))
491        {
492            Ok((0, _)) => Ok(()),
493            Ok(_) => Err(RemoteError::new(RemoteErrorType::DirectoryNotEmpty)),
494            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
495        }
496    }
497
498    fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
499        self.check_connection()?;
500        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
501        if !self.exists(path.as_path()).ok().unwrap_or(false) {
502            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
503        }
504        debug!("Removing directory {} recursively", path.display());
505        match self
506            .session
507            .as_mut()
508            .unwrap()
509            .cmd(format!("rm -rf \"{}\"", path.display()))
510        {
511            Ok((0, _)) => Ok(()),
512            Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
513            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
514        }
515    }
516
517    fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
518        self.check_connection()?;
519        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
520        if self.exists(path.as_path()).ok().unwrap_or(false) {
521            return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
522        }
523        let mode = format!("{:o}", u32::from(mode));
524        debug!(
525            "Creating directory at {} with mode {}",
526            path.display(),
527            mode
528        );
529        match self.session.as_mut().unwrap().cmd(format!(
530            "mkdir -m {} \"{}\"",
531            mode,
532            path.display()
533        )) {
534            Ok((0, _)) => Ok(()),
535            Ok(_) => Err(RemoteError::new(RemoteErrorType::FileCreateDenied)),
536            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
537        }
538    }
539
540    fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> {
541        self.check_connection()?;
542        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
543        debug!(
544            "Creating a symlink at {} pointing at {}",
545            path.display(),
546            target.display()
547        );
548        if !self.exists(target).ok().unwrap_or(false) {
549            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
550        }
551        if self.exists(path.as_path()).ok().unwrap_or(false) {
552            return Err(RemoteError::new(RemoteErrorType::FileCreateDenied));
553        }
554        match self.session.as_mut().unwrap().cmd(format!(
555            "ln -s \"{}\" \"{}\"",
556            target.display(),
557            path.display()
558        )) {
559            Ok((0, _)) => Ok(()),
560            Ok(_) => Err(RemoteError::new(RemoteErrorType::FileCreateDenied)),
561            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
562        }
563    }
564
565    fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
566        self.check_connection()?;
567        let src = path_utils::absolutize(self.wrkdir.as_path(), src);
568        // check if file exists
569        if !self.exists(src.as_path()).ok().unwrap_or(false) {
570            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
571        }
572        let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
573        debug!("Copying {} to {}", src.display(), dest.display());
574        match self
575            .session
576            .as_mut()
577            .unwrap()
578            .cmd(format!("cp -rf \"{}\" \"{}\"", src.display(), dest.display()).as_str())
579        {
580            Ok((0, _)) => Ok(()),
581            Ok(_) => Err(RemoteError::new_ex(
582                // Could not copy file
583                RemoteErrorType::FileCreateDenied,
584                format!("\"{}\"", dest.display()),
585            )),
586            Err(err) => Err(RemoteError::new_ex(
587                RemoteErrorType::ProtocolError,
588                err.to_string(),
589            )),
590        }
591    }
592
593    fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
594        self.check_connection()?;
595        let src = path_utils::absolutize(self.wrkdir.as_path(), src);
596        // check if file exists
597        if !self.exists(src.as_path()).ok().unwrap_or(false) {
598            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
599        }
600        let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
601        debug!("Moving {} to {}", src.display(), dest.display());
602        match self
603            .session
604            .as_mut()
605            .unwrap()
606            .cmd(format!("mv -f \"{}\" \"{}\"", src.display(), dest.display()).as_str())
607        {
608            Ok((0, _)) => Ok(()),
609            Ok(_) => Err(RemoteError::new_ex(
610                // Could not copy file
611                RemoteErrorType::FileCreateDenied,
612                format!("\"{}\"", dest.display()),
613            )),
614            Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
615        }
616    }
617
618    fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> {
619        self.check_connection()?;
620        debug!(r#"Executing command "{cmd}""#);
621        self.session
622            .as_mut()
623            .unwrap()
624            .cmd_at(cmd, self.wrkdir.as_path())
625    }
626
627    fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
628        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
629    }
630
631    fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
632        self.check_connection()?;
633        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
634        debug!("Creating file {}", path.display());
635        trace!("blocked channel");
636        let mode = metadata.mode.map(u32::from).unwrap_or(0o644) as i32;
637        let accessed = metadata
638            .accessed
639            .unwrap_or(SystemTime::UNIX_EPOCH)
640            .duration_since(SystemTime::UNIX_EPOCH)
641            .ok()
642            .unwrap_or(Duration::ZERO)
643            .as_secs();
644        let modified = metadata
645            .modified
646            .unwrap_or(SystemTime::UNIX_EPOCH)
647            .duration_since(SystemTime::UNIX_EPOCH)
648            .ok()
649            .unwrap_or(Duration::ZERO)
650            .as_secs();
651        trace!("Creating file with mode {mode:o}, accessed: {accessed}, modified: {modified}");
652        match self.session.as_mut().unwrap().scp_send(
653            path.as_path(),
654            mode,
655            metadata.size,
656            Some((modified, accessed)),
657        ) {
658            Ok(channel) => Ok(WriteStream::from(channel)),
659            Err(err) => {
660                error!("Failed to create file: {err}");
661                Err(RemoteError::new_ex(RemoteErrorType::FileCreateDenied, err))
662            }
663        }
664    }
665
666    fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
667        self.check_connection()?;
668        let path = path_utils::absolutize(self.wrkdir.as_path(), path);
669        debug!("Opening file {} for read", path.display());
670        // check if file exists
671        if !self.exists(path.as_path()).ok().unwrap_or(false) {
672            return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
673        }
674        trace!("blocked channel");
675        match self.session.as_mut().unwrap().scp_recv(path.as_path()) {
676            Ok(channel) => Ok(ReadStream::from(channel)),
677            Err(err) => {
678                error!("Failed to open file: {err}");
679                Err(RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, err))
680            }
681        }
682    }
683}
684
685#[cfg(test)]
686mod tests;