//! ## Remote Bridged Host //! //! Bridges a `RemoteFs` implementation behind the local host interface used by //! the file transfer activity. mod temp_mapped_file; use std::io::{Read, Write}; use std::path::{Component, Path, PathBuf}; use remotefs::fs::{Metadata, UnixPex}; use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs}; use self::temp_mapped_file::TempMappedFile; use super::{HostBridge, HostError, HostResult}; use crate::utils::path::normalize; struct WriteStreamOp { path: PathBuf, metadata: Metadata, tempfile: TempMappedFile, } /// A remote host bridged over the local host pub struct RemoteBridged { /// Remote fs client remote: Box, /// Reminder used to finalize write stream write_stream_op: Option, } impl RemoteBridged { fn open_file_from_temp(&mut self, file: &Path) -> HostResult> { let mut temp_file = TempMappedFile::new()?; self.remote .open_file(file, Box::new(temp_file.clone())) .map_err(HostError::from)?; // Sync changes temp_file.sync()?; // now return as read Ok(Box::new(temp_file)) } } impl From> for RemoteBridged { fn from(remote: Box) -> Self { RemoteBridged { remote, write_stream_op: None, } } } impl HostBridge for RemoteBridged { fn connect(&mut self) -> HostResult<()> { self.remote.connect().map(|_| ()).map_err(HostError::from) } fn disconnect(&mut self) -> HostResult<()> { self.remote.disconnect().map_err(HostError::from) } fn is_connected(&mut self) -> bool { self.remote.is_connected() } fn is_localhost(&self) -> bool { false } fn pwd(&mut self) -> HostResult { debug!("Getting working directory"); self.remote.pwd().map_err(HostError::from) } fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult { debug!("Changing working directory to {:?}", new_dir); self.remote.change_dir(new_dir).map_err(HostError::from) } fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> { debug!("Creating directory {:?}", dir_name); match self.remote.create_dir(dir_name, UnixPex::from(0o755)) { Ok(_) => Ok(()), Err(remotefs::RemoteError { kind: RemoteErrorType::DirectoryAlreadyExists, .. }) if ignore_existing => Ok(()), Err(e) => Err(HostError::from(e)), } } fn remove(&mut self, entry: &File) -> HostResult<()> { debug!("Removing {:?}", entry.path()); if entry.is_dir() { self.remote .remove_dir_all(entry.path()) .map_err(HostError::from) } else { self.remote .remove_file(entry.path()) .map_err(HostError::from) } } fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> { debug!("Renaming {:?} to {:?}", entry.path(), dst_path); self.remote .mov(entry.path(), dst_path) .map_err(HostError::from) } fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> { debug!("Copying {:?} to {:?}", entry.path(), dst); self.remote.copy(entry.path(), dst).map_err(HostError::from) } fn stat(&mut self, path: &Path) -> HostResult { debug!("Statting {:?}", path); self.remote.stat(path).map_err(HostError::from) } fn exists(&mut self, path: &Path) -> HostResult { debug!("Checking existence of {:?}", path); self.remote.exists(path).map_err(HostError::from) } fn list_dir(&mut self, path: &Path) -> HostResult> { debug!("Listing directory {:?}", path); let entries = self.remote.list_dir(path).map_err(HostError::from)?; Ok(filter_self_refs(path, entries)) } fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> { debug!("Setting metadata for {:?}", path); self.remote .setstat(path, metadata.clone()) .map_err(HostError::from) } fn exec(&mut self, cmd: &str) -> HostResult { debug!("Executing command: {}", cmd); self.remote .exec(cmd) .map(|(_, stdout)| stdout) .map_err(HostError::from) } fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> { debug!("Creating symlink from {:?} to {:?}", src, dst); self.remote.symlink(src, dst).map_err(HostError::from) } fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> { debug!("Changing permissions of {:?} to {:?}", path, pex); let stat = self.remote.stat(path).map_err(HostError::from)?; let mut metadata = stat.metadata.clone(); metadata.mode = Some(pex); self.setstat(path, &metadata) } fn open_file(&mut self, file: &Path) -> HostResult> { // try to use stream, otherwise download to a temporary file and return a reader match self.remote.open(file) { Ok(stream) => Ok(Box::new(stream)), Err(RemoteError { kind: RemoteErrorType::UnsupportedFeature, .. }) => self.open_file_from_temp(file), Err(e) => Err(HostError::from(e)), } } fn create_file( &mut self, file: &Path, metadata: &Metadata, ) -> HostResult> { // try to use stream, otherwise download to a temporary file and return a reader match self.remote.create(file, metadata) { Ok(stream) => Ok(Box::new(stream)), Err(RemoteError { kind: RemoteErrorType::UnsupportedFeature, .. }) => { let tempfile = TempMappedFile::new()?; self.write_stream_op = Some(WriteStreamOp { path: file.to_path_buf(), metadata: metadata.clone(), tempfile: tempfile.clone(), }); Ok(Box::new(tempfile)) } Err(e) => Err(HostError::from(e)), } } fn finalize_write(&mut self, _writer: Box) -> HostResult<()> { if let Some(WriteStreamOp { path, metadata, mut tempfile, }) = self.write_stream_op.take() { // sync tempfile.sync()?; // write file self.remote .create_file(&path, &metadata, Box::new(tempfile))?; } Ok(()) } } /// Drop entries that refer to the directory being listed. /// /// Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference /// to the listed directory in the LIST response, which would otherwise appear /// as a duplicate entry in the explorer. fn filter_self_refs(path: &Path, entries: Vec) -> Vec { let normalized = normalize(path); entries .into_iter() .filter(|entry| { let last = entry.path().components().next_back(); let is_dot_ref = matches!(last, Some(Component::CurDir | Component::ParentDir)); !is_dot_ref && normalize(entry.path()) != normalized }) .collect() } #[cfg(test)] mod test { use std::path::PathBuf; use std::time::SystemTime; use pretty_assertions::assert_eq; use remotefs::fs::{FileType, Metadata}; use super::*; fn file(path: &str, file_type: FileType) -> File { File { path: PathBuf::from(path), metadata: Metadata { accessed: Some(SystemTime::UNIX_EPOCH), created: Some(SystemTime::UNIX_EPOCH), modified: Some(SystemTime::UNIX_EPOCH), file_type, gid: None, mode: None, size: 0, symlink: None, uid: None, }, } } #[test] fn filter_self_refs_drops_entry_matching_listed_dir() { let entries = vec![ file("/wp-content/wp-content", FileType::Directory), file("/wp-content/index.php", FileType::File), ]; let filtered = filter_self_refs(Path::new("/wp-content/wp-content"), entries); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].path(), Path::new("/wp-content/index.php")); } #[test] fn filter_self_refs_drops_dot_and_dotdot_entries() { let entries = vec![ file("/foo/.", FileType::Directory), file("/foo/..", FileType::Directory), file("/foo/bar", FileType::File), ]; let filtered = filter_self_refs(Path::new("/foo"), entries); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].path(), Path::new("/foo/bar")); } #[test] fn filter_self_refs_normalizes_paths() { let entries = vec![ file("/foo/./bar", FileType::File), file("/foo/baz/../", FileType::Directory), ]; let filtered = filter_self_refs(Path::new("/foo"), entries); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].path(), Path::new("/foo/./bar")); } #[test] fn filter_self_refs_preserves_unrelated_entries() { let entries = vec![ file("/home/user/notes.txt", FileType::File), file("/home/user/photos", FileType::Directory), ]; let filtered = filter_self_refs(Path::new("/home/user"), entries.clone()); assert_eq!(filtered.len(), 2); } }