Skip to main content

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