mirror of
https://github.com/veeso/termscp.git
synced 2026-07-11 18:31:26 +02:00
Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference to the listed directory in the LIST response, causing the current folder to appear as a duplicate entry in the explorer. Closes #410
316 lines
9.6 KiB
Rust
316 lines
9.6 KiB
Rust
//! ## 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<dyn RemoteFs>,
|
|
/// Reminder used to finalize write stream
|
|
write_stream_op: Option<WriteStreamOp>,
|
|
}
|
|
|
|
impl RemoteBridged {
|
|
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
|
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<Box<dyn RemoteFs>> for RemoteBridged {
|
|
fn from(remote: Box<dyn RemoteFs>) -> 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<PathBuf> {
|
|
debug!("Getting working directory");
|
|
self.remote.pwd().map_err(HostError::from)
|
|
}
|
|
|
|
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {
|
|
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<File> {
|
|
debug!("Statting {:?}", path);
|
|
self.remote.stat(path).map_err(HostError::from)
|
|
}
|
|
|
|
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
|
debug!("Checking existence of {:?}", path);
|
|
self.remote.exists(path).map_err(HostError::from)
|
|
}
|
|
|
|
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
|
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<String> {
|
|
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<Box<dyn Read + Send>> {
|
|
// 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<Box<dyn Write + Send>> {
|
|
// 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<dyn Write + Send>) -> 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<File>) -> Vec<File> {
|
|
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);
|
|
}
|
|
}
|