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(&mut self, dir: &Path, entries: &[&str]) -> HashMap<String, SystemTime> {
317 if entries.is_empty() {
318 return HashMap::new();
319 }
320 let fmt = match self.stat_flavor() {
321 StatFlavor::Gnu => "-c '%Y %n'",
322 StatFlavor::Bsd => "-f '%m %N'",
323 StatFlavor::Unsupported => return HashMap::new(),
324 };
325 let args: Vec<String> = entries
326 .iter()
327 .map(|name| format!("\"{}\"", dir.join(name).display()))
328 .collect();
329 let cmd = format!("stat {} {}", fmt, args.join(" "));
330 match self.session.as_mut().unwrap().cmd(cmd) {
331 Ok((_, output)) => parser_utils::parse_stat_listing(&output),
332 Err(err) => {
333 warn!("Batched stat failed, falling back to ls timestamps: {err}");
334 HashMap::new()
335 }
336 }
337 }
338}
339
340impl<S> RemoteFs for ScpFs<S>
341where
342 S: SshSession,
343{
344 fn connect(&mut self) -> RemoteResult<Welcome> {
345 debug!("Initializing SFTP connection...");
346 let mut session = S::connect(&self.opts)?;
347 let banner = session.banner()?;
349 debug!(
350 "Connection established: {}",
351 banner.as_deref().unwrap_or("")
352 );
353 debug!("Getting working directory...");
355 self.wrkdir = session
356 .cmd("pwd")
357 .map(|(_rc, output)| PathBuf::from(output.as_str().trim()))?;
358 self.session = Some(session);
360 info!(
361 "Connection established; working directory: {}",
362 self.wrkdir.display()
363 );
364 Ok(Welcome::default().banner(banner))
365 }
366
367 fn disconnect(&mut self) -> RemoteResult<()> {
368 debug!("Disconnecting from remote...");
369 if let Some(session) = self.session.as_ref() {
370 match session.disconnect() {
372 Ok(_) => {
373 self.session = None;
375 self.stat_flavor = None;
377 Ok(())
378 }
379 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err)),
380 }
381 } else {
382 Err(RemoteError::new(RemoteErrorType::NotConnected))
383 }
384 }
385
386 fn is_connected(&mut self) -> bool {
387 self.session
388 .as_ref()
389 .map(|x| x.authenticated().unwrap_or_default())
390 .unwrap_or(false)
391 }
392
393 fn pwd(&mut self) -> RemoteResult<PathBuf> {
394 self.check_connection()?;
395 Ok(self.wrkdir.clone())
396 }
397
398 fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
399 self.check_connection()?;
400 let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
401 debug!("Changing working directory to {}", dir.display());
402 match self
403 .session
404 .as_mut()
405 .unwrap()
406 .cmd(format!("cd \"{}\"; echo $?; pwd", dir.display()))
407 {
408 Ok((rc, output)) => {
409 if rc != 0 {
410 return Err(RemoteError::new_ex(
411 RemoteErrorType::ProtocolError,
412 format!("Failed to change directory: {}", output),
413 ));
414 }
415 let output: String = String::from(output.as_str().trim());
417 match output.as_str().starts_with('0') {
419 true => {
420 self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
422 debug!("Changed working directory to {}", self.wrkdir.display());
423 Ok(self.wrkdir.clone())
424 }
425 false => Err(RemoteError::new_ex(
426 RemoteErrorType::NoSuchFileOrDirectory,
428 format!("\"{}\"", dir.display()),
429 )),
430 }
431 }
432 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
433 }
434 }
435
436 fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
437 self.check_connection()?;
438 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
439 debug!("Getting file entries in {}", path.display());
440 if !self.exists(path.as_path()).ok().unwrap_or(false) {
442 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
443 }
444 match self
445 .session
446 .as_mut()
447 .unwrap()
448 .cmd(format!("unset LANG; ls -la \"{}/\"", path.display()).as_str())
449 {
450 Ok((rc, output)) => {
451 if rc != 0 {
452 return Err(RemoteError::new_ex(
453 RemoteErrorType::ProtocolError,
454 format!("Failed to list directory: {}", output),
455 ));
456 }
457 let lines: Vec<&str> = output.as_str().lines().collect();
459 let mut entries: Vec<File> = Vec::with_capacity(lines.len());
460 for line in lines.iter() {
461 if let Ok(entry) = self.parse_ls_output(path.as_path(), line) {
464 entries.push(entry);
465 }
466 }
467 let names: Vec<String> = entries.iter().map(|e| e.name()).collect();
470 let name_refs: Vec<&str> = names.iter().map(String::as_str).collect();
471 let mtimes = self.mtimes_in_dir(path.as_path(), &name_refs);
472 for (entry, name) in entries.iter_mut().zip(names.iter()) {
473 if let Some(t) = mtimes.get(name) {
474 entry.metadata.modified = Some(*t);
475 }
476 }
477 debug!(
478 "Found {} out of {} valid file entries",
479 entries.len(),
480 lines.len()
481 );
482 Ok(entries)
483 }
484 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
485 }
486 }
487
488 fn stat(&mut self, path: &Path) -> RemoteResult<File> {
489 self.check_connection()?;
490 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
491 debug!("Stat {}", path.display());
492 let cmd = match self.is_directory(path.as_path())? {
494 true => format!("ls -ld \"{}\"", path.display()),
495 false => format!("ls -l \"{}\"", path.display()),
496 };
497 match self.session.as_mut().unwrap().cmd(cmd.as_str()) {
498 Ok((rc, line)) => {
499 if rc != 0 {
500 return Err(RemoteError::new_ex(
501 RemoteErrorType::NoSuchFileOrDirectory,
502 format!("Failed to stat file: {line}"),
503 ));
504 }
505 let parent: PathBuf = match path.as_path().parent() {
507 Some(p) => PathBuf::from(p),
508 None => {
509 return Err(RemoteError::new_ex(
510 RemoteErrorType::StatFailed,
511 "Path has no parent",
512 ));
513 }
514 };
515 match self.parse_ls_output(parent.as_path(), line.as_str().trim()) {
516 Ok(mut entry) => {
517 if let Some(t) = self.mtime_epoch(path.as_path()) {
519 entry.metadata.modified = Some(t);
520 }
521 Ok(entry)
522 }
523 Err(_) => Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory)),
524 }
525 }
526 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
527 }
528 }
529
530 fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
531 self.check_connection()?;
532 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
533 match self
534 .session
535 .as_mut()
536 .unwrap()
537 .cmd(format!("test -e \"{}\"", path.display()))
538 {
539 Ok((0, _)) => Ok(true),
540 Ok(_) => Ok(false),
541 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
542 }
543 }
544
545 fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
546 self.check_connection()?;
547 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
548 debug!("Setting attributes for {}", path.display());
549 if !self.exists(path.as_path()).ok().unwrap_or(false) {
550 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
551 }
552 if let Some(mode) = metadata.mode {
554 self.assert_stat_command(format!(
555 "chmod {:o} \"{}\"",
556 u32::from(mode),
557 path.display()
558 ))?;
559 }
560 if let Some(user) = metadata.uid {
561 self.assert_stat_command(format!(
562 "chown {}{} \"{}\"",
563 user,
564 metadata.gid.map(|x| format!(":{x}")).unwrap_or_default(),
565 path.display()
566 ))?;
567 }
568 if let Some(accessed) = metadata.accessed {
570 self.assert_stat_command(format!(
571 "touch -a -t {} \"{}\"",
572 fmt_utils::fmt_time_utc(accessed, "%Y%m%d%H%M.%S"),
573 path.display()
574 ))?;
575 }
576 if let Some(modified) = metadata.modified {
577 self.assert_stat_command(format!(
578 "touch -m -t {} \"{}\"",
579 fmt_utils::fmt_time_utc(modified, "%Y%m%d%H%M.%S"),
580 path.display()
581 ))?;
582 }
583 Ok(())
584 }
585
586 fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
587 self.check_connection()?;
588 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
589 if !self.exists(path.as_path()).ok().unwrap_or(false) {
590 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
591 }
592 debug!("Removing file {}", path.display());
593 match self
594 .session
595 .as_mut()
596 .unwrap()
597 .cmd(format!("rm -f \"{}\"", path.display()))
598 {
599 Ok((0, _)) => Ok(()),
600 Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
601 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
602 }
603 }
604
605 fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
606 self.check_connection()?;
607 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
608 if !self.exists(path.as_path()).ok().unwrap_or(false) {
609 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
610 }
611 debug!("Removing directory {}", path.display());
612 match self
613 .session
614 .as_mut()
615 .unwrap()
616 .cmd(format!("rmdir \"{}\"", path.display()))
617 {
618 Ok((0, _)) => Ok(()),
619 Ok(_) => Err(RemoteError::new(RemoteErrorType::DirectoryNotEmpty)),
620 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
621 }
622 }
623
624 fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
625 self.check_connection()?;
626 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
627 if !self.exists(path.as_path()).ok().unwrap_or(false) {
628 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
629 }
630 debug!("Removing directory {} recursively", path.display());
631 match self
632 .session
633 .as_mut()
634 .unwrap()
635 .cmd(format!("rm -rf \"{}\"", path.display()))
636 {
637 Ok((0, _)) => Ok(()),
638 Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
639 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
640 }
641 }
642
643 fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
644 self.check_connection()?;
645 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
646 if self.exists(path.as_path()).ok().unwrap_or(false) {
647 return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
648 }
649 let mode = format!("{:o}", u32::from(mode));
650 debug!(
651 "Creating directory at {} with mode {}",
652 path.display(),
653 mode
654 );
655 match self.session.as_mut().unwrap().cmd(format!(
656 "mkdir -m {} \"{}\"",
657 mode,
658 path.display()
659 )) {
660 Ok((0, _)) => Ok(()),
661 Ok(_) => Err(RemoteError::new(RemoteErrorType::FileCreateDenied)),
662 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
663 }
664 }
665
666 fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> {
667 self.check_connection()?;
668 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
669 debug!(
670 "Creating a symlink at {} pointing at {}",
671 path.display(),
672 target.display()
673 );
674 if !self.exists(target).ok().unwrap_or(false) {
675 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
676 }
677 if self.exists(path.as_path()).ok().unwrap_or(false) {
678 return Err(RemoteError::new(RemoteErrorType::FileCreateDenied));
679 }
680 match self.session.as_mut().unwrap().cmd(format!(
681 "ln -s \"{}\" \"{}\"",
682 target.display(),
683 path.display()
684 )) {
685 Ok((0, _)) => Ok(()),
686 Ok(_) => Err(RemoteError::new(RemoteErrorType::FileCreateDenied)),
687 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
688 }
689 }
690
691 fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
692 self.check_connection()?;
693 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
694 if !self.exists(src.as_path()).ok().unwrap_or(false) {
696 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
697 }
698 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
699 debug!("Copying {} to {}", src.display(), dest.display());
700 match self
701 .session
702 .as_mut()
703 .unwrap()
704 .cmd(format!("cp -rf \"{}\" \"{}\"", src.display(), dest.display()).as_str())
705 {
706 Ok((0, _)) => Ok(()),
707 Ok(_) => Err(RemoteError::new_ex(
708 RemoteErrorType::FileCreateDenied,
710 format!("\"{}\"", dest.display()),
711 )),
712 Err(err) => Err(RemoteError::new_ex(
713 RemoteErrorType::ProtocolError,
714 err.to_string(),
715 )),
716 }
717 }
718
719 fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
720 self.check_connection()?;
721 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
722 if !self.exists(src.as_path()).ok().unwrap_or(false) {
724 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
725 }
726 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
727 debug!("Moving {} to {}", src.display(), dest.display());
728 match self
729 .session
730 .as_mut()
731 .unwrap()
732 .cmd(format!("mv -f \"{}\" \"{}\"", src.display(), dest.display()).as_str())
733 {
734 Ok((0, _)) => Ok(()),
735 Ok(_) => Err(RemoteError::new_ex(
736 RemoteErrorType::FileCreateDenied,
738 format!("\"{}\"", dest.display()),
739 )),
740 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
741 }
742 }
743
744 fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> {
745 self.check_connection()?;
746 debug!(r#"Executing command "{cmd}""#);
747 self.session
748 .as_mut()
749 .unwrap()
750 .cmd_at(cmd, self.wrkdir.as_path())
751 }
752
753 fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
754 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
755 }
756
757 fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
758 self.check_connection()?;
759 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
760 debug!("Creating file {}", path.display());
761 trace!("blocked channel");
762 let mode = metadata.mode.map(u32::from).unwrap_or(0o644) as i32;
763 let accessed = metadata
764 .accessed
765 .unwrap_or(SystemTime::UNIX_EPOCH)
766 .duration_since(SystemTime::UNIX_EPOCH)
767 .ok()
768 .unwrap_or(Duration::ZERO)
769 .as_secs();
770 let modified = metadata
771 .modified
772 .unwrap_or(SystemTime::UNIX_EPOCH)
773 .duration_since(SystemTime::UNIX_EPOCH)
774 .ok()
775 .unwrap_or(Duration::ZERO)
776 .as_secs();
777 trace!("Creating file with mode {mode:o}, accessed: {accessed}, modified: {modified}");
778 match self.session.as_mut().unwrap().scp_send(
779 path.as_path(),
780 mode,
781 metadata.size,
782 Some((modified, accessed)),
783 ) {
784 Ok(channel) => Ok(WriteStream::from(channel)),
785 Err(err) => {
786 error!("Failed to create file: {err}");
787 Err(RemoteError::new_ex(RemoteErrorType::FileCreateDenied, err))
788 }
789 }
790 }
791
792 fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
793 self.check_connection()?;
794 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
795 debug!("Opening file {} for read", path.display());
796 if !self.exists(path.as_path()).ok().unwrap_or(false) {
798 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
799 }
800 trace!("blocked channel");
801 match self.session.as_mut().unwrap().scp_recv(path.as_path()) {
802 Ok(channel) => Ok(ReadStream::from(channel)),
803 Err(err) => {
804 error!("Failed to open file: {err}");
805 Err(RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, err))
806 }
807 }
808 }
809}
810
811#[cfg(test)]
812mod tests;