1use std::collections::HashMap;
6use std::ops::Range;
7use std::path::{Path, PathBuf};
8use std::time::{Duration, SystemTime};
9
10use lazy_regex::{Lazy, Regex};
11use remotefs::File;
12use remotefs::fs::{
13 FileType, Metadata, ReadStream, RemoteError, RemoteErrorType, RemoteFs, RemoteResult, UnixPex,
14 UnixPexClass, Welcome, WriteStream,
15};
16
17use super::SshOpts;
18use crate::SshSession;
19use crate::utils::{fmt as fmt_utils, parser as parser_utils, path as path_utils};
20
21static LS_RE: Lazy<Regex> = lazy_regex!(
23 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>.+)$"#
24);
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum StatFlavor {
34 Gnu,
36 Bsd,
38 Unsupported,
40}
41
42pub struct ScpFs<S>
44where
45 S: SshSession,
46{
47 session: Option<S>,
48 wrkdir: PathBuf,
49 opts: SshOpts,
50 stat_flavor: Option<StatFlavor>,
52}
53
54#[cfg(feature = "libssh2")]
55#[cfg_attr(docsrs, doc(cfg(feature = "libssh2")))]
56impl ScpFs<super::backend::LibSsh2Session> {
57 pub fn libssh2(opts: SshOpts) -> Self {
59 Self {
60 session: None,
61 wrkdir: PathBuf::from("/"),
62 opts,
63 stat_flavor: None,
64 }
65 }
66}
67
68#[cfg(feature = "libssh")]
69#[cfg_attr(docsrs, doc(cfg(feature = "libssh")))]
70impl ScpFs<super::backend::LibSshSession> {
71 pub fn libssh(opts: SshOpts) -> Self {
73 Self {
74 session: None,
75 wrkdir: PathBuf::from("/"),
76 opts,
77 stat_flavor: None,
78 }
79 }
80}
81
82#[cfg(feature = "russh")]
83#[cfg_attr(docsrs, doc(cfg(feature = "russh")))]
84impl<T> ScpFs<super::backend::RusshSession<T>>
85where
86 T: russh::client::Handler + Default + Send + 'static,
87{
88 pub fn russh(opts: SshOpts, runtime: std::sync::Arc<tokio::runtime::Runtime>) -> Self {
90 let opts = opts.runtime(runtime);
91 Self {
92 session: None,
93 wrkdir: PathBuf::from("/"),
94 opts,
95 stat_flavor: None,
96 }
97 }
98}
99
100impl<S> ScpFs<S>
101where
102 S: SshSession,
103{
104 pub fn session(&mut self) -> Option<&mut S> {
106 self.session.as_mut()
107 }
108
109 fn check_connection(&mut self) -> RemoteResult<()> {
113 if self.is_connected() {
114 Ok(())
115 } else {
116 Err(RemoteError::new(RemoteErrorType::NotConnected))
117 }
118 }
119
120 fn parse_ls_output(&self, path: &Path, line: &str) -> Result<File, ()> {
122 trace!("Parsing LS line: '{line}'");
124 match LS_RE.captures(line) {
126 Some(metadata) => {
128 if metadata.len() < 8 {
131 return Err(());
132 }
133 let (is_dir, is_symlink): (bool, bool) = match &metadata["sym_dir"] {
137 "-" => (false, false),
138 "l" => (false, true),
139 "d" => (true, false),
140 _ => return Err(()), };
142 if metadata["pex"].len() < 9 {
144 return Err(());
145 }
146
147 let pex = |range: Range<usize>| {
148 let mut count: u8 = 0;
149 for (i, c) in metadata["pex"][range].chars().enumerate() {
150 match c {
151 '-' => {}
152 _ => {
153 count += match i {
154 0 => 4,
155 1 => 2,
156 2 => 1,
157 _ => 0,
158 }
159 }
160 }
161 }
162 count
163 };
164
165 let mode = UnixPex::new(
167 UnixPexClass::from(pex(0..3)),
168 UnixPexClass::from(pex(3..6)),
169 UnixPexClass::from(pex(6..9)),
170 );
171
172 let modified: SystemTime = match parser_utils::parse_lstime(
174 &metadata["date_time"],
175 "%b %d %Y",
176 "%b %d %H:%M",
177 ) {
178 Ok(t) => t,
179 Err(_) => SystemTime::UNIX_EPOCH,
180 };
181 let uid: Option<u32> = metadata["uid"].parse::<u32>().ok();
183 let gid: Option<u32> = metadata["gid"].parse::<u32>().ok();
185 let size = metadata["size"].parse::<u64>().unwrap_or(0);
187 let (file_name, symlink): (String, Option<PathBuf>) = match is_symlink {
189 true => self.get_name_and_link(&metadata["name"]),
190 false => (String::from(&metadata["name"]), None),
191 };
192 let file_name = PathBuf::from(&file_name)
194 .file_name()
195 .map(|x| x.to_string_lossy().to_string())
196 .unwrap_or(file_name);
197 if file_name.as_str() == "." || file_name.as_str() == ".." {
199 return Err(());
200 }
201 let mut path: PathBuf = path.to_path_buf();
203 path.push(file_name.as_str());
204 let file_type = if symlink.is_some() {
206 FileType::Symlink
207 } else if is_dir {
208 FileType::Directory
209 } else {
210 FileType::File
211 };
212 let metadata = Metadata {
214 accessed: None,
215 created: None,
216 file_type,
217 gid,
218 mode: Some(mode),
219 modified: Some(modified),
220 size,
221 symlink,
222 uid,
223 };
224 trace!(
225 "Found entry at {} with metadata {:?}",
226 path.display(),
227 metadata
228 );
229 Ok(File { path, metadata })
231 }
232 None => Err(()),
233 }
234 }
235
236 fn get_name_and_link(&self, token: &str) -> (String, Option<PathBuf>) {
240 let tokens: Vec<&str> = token.split(" -> ").collect();
241 let filename: String = String::from(*tokens.first().unwrap());
242 let symlink: Option<PathBuf> = tokens.get(1).map(PathBuf::from);
243 (filename, symlink)
244 }
245
246 fn assert_stat_command(&mut self, cmd: String) -> RemoteResult<()> {
248 match self.session.as_mut().unwrap().cmd(cmd) {
249 Ok((0, _)) => Ok(()),
250 Ok(_) => Err(RemoteError::new(RemoteErrorType::StatFailed)),
251 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
252 }
253 }
254
255 fn is_directory(&mut self, path: &Path) -> RemoteResult<bool> {
257 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
258 match self
259 .session
260 .as_mut()
261 .unwrap()
262 .cmd(format!("test -d \"{}\"", path.display()))
263 {
264 Ok((0, _)) => Ok(true),
265 Ok(_) => Ok(false),
266 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
267 }
268 }
269
270 fn stat_flavor(&mut self) -> StatFlavor {
278 if let Some(flavor) = self.stat_flavor {
279 return flavor;
280 }
281 let session = self.session.as_mut().unwrap();
282 let flavor = match session.cmd("stat --version >/dev/null 2>&1") {
283 Ok((0, _)) => StatFlavor::Gnu,
284 _ => match session.cmd("stat -f %m / >/dev/null 2>&1") {
285 Ok((0, _)) => StatFlavor::Bsd,
286 _ => StatFlavor::Unsupported,
287 },
288 };
289 trace!("Detected remote stat flavor: {flavor:?}");
290 self.stat_flavor = Some(flavor);
291 flavor
292 }
293
294 fn mtime_epoch(&mut self, path: &Path) -> Option<SystemTime> {
299 let flag = match self.stat_flavor() {
300 StatFlavor::Gnu => "-c %Y",
301 StatFlavor::Bsd => "-f %m",
302 StatFlavor::Unsupported => return None,
303 };
304 let cmd = format!("stat {} \"{}\"", flag, path.display());
305 match self.session.as_mut().unwrap().cmd(cmd) {
306 Ok((0, output)) => parser_utils::parse_stat_epoch(&output),
307 _ => None,
308 }
309 }
310
311 fn mtimes_in_dir(
317 &mut self,
318 dir: &Path,
319 entries: &[&str],
320 ) -> HashMap<String, SystemTime> {
321 if entries.is_empty() {
322 return HashMap::new();
323 }
324 let fmt = match self.stat_flavor() {
325 StatFlavor::Gnu => "-c '%Y %n'",
326 StatFlavor::Bsd => "-f '%m %N'",
327 StatFlavor::Unsupported => return HashMap::new(),
328 };
329 let args: Vec<String> = entries
330 .iter()
331 .map(|name| format!("\"{}\"", dir.join(name).display()))
332 .collect();
333 let cmd = format!("stat {} {}", fmt, args.join(" "));
334 match self.session.as_mut().unwrap().cmd(cmd) {
335 Ok((_, output)) => parser_utils::parse_stat_listing(&output),
336 Err(err) => {
337 warn!("Batched stat failed, falling back to ls timestamps: {err}");
338 HashMap::new()
339 }
340 }
341 }
342}
343
344impl<S> RemoteFs for ScpFs<S>
345where
346 S: SshSession,
347{
348 fn connect(&mut self) -> RemoteResult<Welcome> {
349 debug!("Initializing SFTP connection...");
350 let mut session = S::connect(&self.opts)?;
351 let banner = session.banner()?;
353 debug!(
354 "Connection established: {}",
355 banner.as_deref().unwrap_or("")
356 );
357 debug!("Getting working directory...");
359 self.wrkdir = session
360 .cmd("pwd")
361 .map(|(_rc, output)| PathBuf::from(output.as_str().trim()))?;
362 self.session = Some(session);
364 info!(
365 "Connection established; working directory: {}",
366 self.wrkdir.display()
367 );
368 Ok(Welcome::default().banner(banner))
369 }
370
371 fn disconnect(&mut self) -> RemoteResult<()> {
372 debug!("Disconnecting from remote...");
373 if let Some(session) = self.session.as_ref() {
374 match session.disconnect() {
376 Ok(_) => {
377 self.session = None;
379 self.stat_flavor = None;
381 Ok(())
382 }
383 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err)),
384 }
385 } else {
386 Err(RemoteError::new(RemoteErrorType::NotConnected))
387 }
388 }
389
390 fn is_connected(&mut self) -> bool {
391 self.session
392 .as_ref()
393 .map(|x| x.authenticated().unwrap_or_default())
394 .unwrap_or(false)
395 }
396
397 fn pwd(&mut self) -> RemoteResult<PathBuf> {
398 self.check_connection()?;
399 Ok(self.wrkdir.clone())
400 }
401
402 fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
403 self.check_connection()?;
404 let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
405 debug!("Changing working directory to {}", dir.display());
406 match self
407 .session
408 .as_mut()
409 .unwrap()
410 .cmd(format!("cd \"{}\"; echo $?; pwd", dir.display()))
411 {
412 Ok((rc, output)) => {
413 if rc != 0 {
414 return Err(RemoteError::new_ex(
415 RemoteErrorType::ProtocolError,
416 format!("Failed to change directory: {}", output),
417 ));
418 }
419 let output: String = String::from(output.as_str().trim());
421 match output.as_str().starts_with('0') {
423 true => {
424 self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
426 debug!("Changed working directory to {}", self.wrkdir.display());
427 Ok(self.wrkdir.clone())
428 }
429 false => Err(RemoteError::new_ex(
430 RemoteErrorType::NoSuchFileOrDirectory,
432 format!("\"{}\"", dir.display()),
433 )),
434 }
435 }
436 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
437 }
438 }
439
440 fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
441 self.check_connection()?;
442 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
443 debug!("Getting file entries in {}", path.display());
444 if !self.exists(path.as_path()).ok().unwrap_or(false) {
446 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
447 }
448 match self
449 .session
450 .as_mut()
451 .unwrap()
452 .cmd(format!("unset LANG; ls -la \"{}/\"", path.display()).as_str())
453 {
454 Ok((rc, output)) => {
455 if rc != 0 {
456 return Err(RemoteError::new_ex(
457 RemoteErrorType::ProtocolError,
458 format!("Failed to list directory: {}", output),
459 ));
460 }
461 let lines: Vec<&str> = output.as_str().lines().collect();
463 let mut entries: Vec<File> = Vec::with_capacity(lines.len());
464 for line in lines.iter() {
465 if let Ok(entry) = self.parse_ls_output(path.as_path(), line) {
468 entries.push(entry);
469 }
470 }
471 let names: Vec<String> = entries.iter().map(|e| e.name()).collect();
474 let name_refs: Vec<&str> = names.iter().map(String::as_str).collect();
475 let mtimes = self.mtimes_in_dir(path.as_path(), &name_refs);
476 for (entry, name) in entries.iter_mut().zip(names.iter()) {
477 if let Some(t) = mtimes.get(name) {
478 entry.metadata.modified = Some(*t);
479 }
480 }
481 debug!(
482 "Found {} out of {} valid file entries",
483 entries.len(),
484 lines.len()
485 );
486 Ok(entries)
487 }
488 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
489 }
490 }
491
492 fn stat(&mut self, path: &Path) -> RemoteResult<File> {
493 self.check_connection()?;
494 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
495 debug!("Stat {}", path.display());
496 let cmd = match self.is_directory(path.as_path())? {
498 true => format!("ls -ld \"{}\"", path.display()),
499 false => format!("ls -l \"{}\"", path.display()),
500 };
501 match self.session.as_mut().unwrap().cmd(cmd.as_str()) {
502 Ok((rc, line)) => {
503 if rc != 0 {
504 return Err(RemoteError::new_ex(
505 RemoteErrorType::NoSuchFileOrDirectory,
506 format!("Failed to stat file: {line}"),
507 ));
508 }
509 let parent: PathBuf = match path.as_path().parent() {
511 Some(p) => PathBuf::from(p),
512 None => {
513 return Err(RemoteError::new_ex(
514 RemoteErrorType::StatFailed,
515 "Path has no parent",
516 ));
517 }
518 };
519 match self.parse_ls_output(parent.as_path(), line.as_str().trim()) {
520 Ok(mut entry) => {
521 if let Some(t) = self.mtime_epoch(path.as_path()) {
523 entry.metadata.modified = Some(t);
524 }
525 Ok(entry)
526 }
527 Err(_) => Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory)),
528 }
529 }
530 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
531 }
532 }
533
534 fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
535 self.check_connection()?;
536 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
537 match self
538 .session
539 .as_mut()
540 .unwrap()
541 .cmd(format!("test -e \"{}\"", path.display()))
542 {
543 Ok((0, _)) => Ok(true),
544 Ok(_) => Ok(false),
545 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
546 }
547 }
548
549 fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
550 self.check_connection()?;
551 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
552 debug!("Setting attributes for {}", path.display());
553 if !self.exists(path.as_path()).ok().unwrap_or(false) {
554 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
555 }
556 if let Some(mode) = metadata.mode {
558 self.assert_stat_command(format!(
559 "chmod {:o} \"{}\"",
560 u32::from(mode),
561 path.display()
562 ))?;
563 }
564 if let Some(user) = metadata.uid {
565 self.assert_stat_command(format!(
566 "chown {}{} \"{}\"",
567 user,
568 metadata.gid.map(|x| format!(":{x}")).unwrap_or_default(),
569 path.display()
570 ))?;
571 }
572 if let Some(accessed) = metadata.accessed {
574 self.assert_stat_command(format!(
575 "touch -a -t {} \"{}\"",
576 fmt_utils::fmt_time_utc(accessed, "%Y%m%d%H%M.%S"),
577 path.display()
578 ))?;
579 }
580 if let Some(modified) = metadata.modified {
581 self.assert_stat_command(format!(
582 "touch -m -t {} \"{}\"",
583 fmt_utils::fmt_time_utc(modified, "%Y%m%d%H%M.%S"),
584 path.display()
585 ))?;
586 }
587 Ok(())
588 }
589
590 fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
591 self.check_connection()?;
592 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
593 if !self.exists(path.as_path()).ok().unwrap_or(false) {
594 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
595 }
596 debug!("Removing file {}", path.display());
597 match self
598 .session
599 .as_mut()
600 .unwrap()
601 .cmd(format!("rm -f \"{}\"", path.display()))
602 {
603 Ok((0, _)) => Ok(()),
604 Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
605 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
606 }
607 }
608
609 fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
610 self.check_connection()?;
611 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
612 if !self.exists(path.as_path()).ok().unwrap_or(false) {
613 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
614 }
615 debug!("Removing directory {}", path.display());
616 match self
617 .session
618 .as_mut()
619 .unwrap()
620 .cmd(format!("rmdir \"{}\"", path.display()))
621 {
622 Ok((0, _)) => Ok(()),
623 Ok(_) => Err(RemoteError::new(RemoteErrorType::DirectoryNotEmpty)),
624 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
625 }
626 }
627
628 fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
629 self.check_connection()?;
630 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
631 if !self.exists(path.as_path()).ok().unwrap_or(false) {
632 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
633 }
634 debug!("Removing directory {} recursively", path.display());
635 match self
636 .session
637 .as_mut()
638 .unwrap()
639 .cmd(format!("rm -rf \"{}\"", path.display()))
640 {
641 Ok((0, _)) => Ok(()),
642 Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
643 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
644 }
645 }
646
647 fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
648 self.check_connection()?;
649 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
650 if self.exists(path.as_path()).ok().unwrap_or(false) {
651 return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
652 }
653 let mode = format!("{:o}", u32::from(mode));
654 debug!(
655 "Creating directory at {} with mode {}",
656 path.display(),
657 mode
658 );
659 match self.session.as_mut().unwrap().cmd(format!(
660 "mkdir -m {} \"{}\"",
661 mode,
662 path.display()
663 )) {
664 Ok((0, _)) => Ok(()),
665 Ok(_) => Err(RemoteError::new(RemoteErrorType::FileCreateDenied)),
666 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
667 }
668 }
669
670 fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> {
671 self.check_connection()?;
672 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
673 debug!(
674 "Creating a symlink at {} pointing at {}",
675 path.display(),
676 target.display()
677 );
678 if !self.exists(target).ok().unwrap_or(false) {
679 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
680 }
681 if self.exists(path.as_path()).ok().unwrap_or(false) {
682 return Err(RemoteError::new(RemoteErrorType::FileCreateDenied));
683 }
684 match self.session.as_mut().unwrap().cmd(format!(
685 "ln -s \"{}\" \"{}\"",
686 target.display(),
687 path.display()
688 )) {
689 Ok((0, _)) => Ok(()),
690 Ok(_) => Err(RemoteError::new(RemoteErrorType::FileCreateDenied)),
691 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
692 }
693 }
694
695 fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
696 self.check_connection()?;
697 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
698 if !self.exists(src.as_path()).ok().unwrap_or(false) {
700 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
701 }
702 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
703 debug!("Copying {} to {}", src.display(), dest.display());
704 match self
705 .session
706 .as_mut()
707 .unwrap()
708 .cmd(format!("cp -rf \"{}\" \"{}\"", src.display(), dest.display()).as_str())
709 {
710 Ok((0, _)) => Ok(()),
711 Ok(_) => Err(RemoteError::new_ex(
712 RemoteErrorType::FileCreateDenied,
714 format!("\"{}\"", dest.display()),
715 )),
716 Err(err) => Err(RemoteError::new_ex(
717 RemoteErrorType::ProtocolError,
718 err.to_string(),
719 )),
720 }
721 }
722
723 fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
724 self.check_connection()?;
725 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
726 if !self.exists(src.as_path()).ok().unwrap_or(false) {
728 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
729 }
730 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
731 debug!("Moving {} to {}", src.display(), dest.display());
732 match self
733 .session
734 .as_mut()
735 .unwrap()
736 .cmd(format!("mv -f \"{}\" \"{}\"", src.display(), dest.display()).as_str())
737 {
738 Ok((0, _)) => Ok(()),
739 Ok(_) => Err(RemoteError::new_ex(
740 RemoteErrorType::FileCreateDenied,
742 format!("\"{}\"", dest.display()),
743 )),
744 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
745 }
746 }
747
748 fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> {
749 self.check_connection()?;
750 debug!(r#"Executing command "{cmd}""#);
751 self.session
752 .as_mut()
753 .unwrap()
754 .cmd_at(cmd, self.wrkdir.as_path())
755 }
756
757 fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
758 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
759 }
760
761 fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
762 self.check_connection()?;
763 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
764 debug!("Creating file {}", path.display());
765 trace!("blocked channel");
766 let mode = metadata.mode.map(u32::from).unwrap_or(0o644) as i32;
767 let accessed = metadata
768 .accessed
769 .unwrap_or(SystemTime::UNIX_EPOCH)
770 .duration_since(SystemTime::UNIX_EPOCH)
771 .ok()
772 .unwrap_or(Duration::ZERO)
773 .as_secs();
774 let modified = metadata
775 .modified
776 .unwrap_or(SystemTime::UNIX_EPOCH)
777 .duration_since(SystemTime::UNIX_EPOCH)
778 .ok()
779 .unwrap_or(Duration::ZERO)
780 .as_secs();
781 trace!("Creating file with mode {mode:o}, accessed: {accessed}, modified: {modified}");
782 match self.session.as_mut().unwrap().scp_send(
783 path.as_path(),
784 mode,
785 metadata.size,
786 Some((modified, accessed)),
787 ) {
788 Ok(channel) => Ok(WriteStream::from(channel)),
789 Err(err) => {
790 error!("Failed to create file: {err}");
791 Err(RemoteError::new_ex(RemoteErrorType::FileCreateDenied, err))
792 }
793 }
794 }
795
796 fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
797 self.check_connection()?;
798 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
799 debug!("Opening file {} for read", path.display());
800 if !self.exists(path.as_path()).ok().unwrap_or(false) {
802 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
803 }
804 trace!("blocked channel");
805 match self.session.as_mut().unwrap().scp_recv(path.as_path()) {
806 Ok(channel) => Ok(ReadStream::from(channel)),
807 Err(err) => {
808 error!("Failed to open file: {err}");
809 Err(RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, err))
810 }
811 }
812 }
813}
814
815#[cfg(test)]
816mod tests;