build(remotefs): migrate to version 1 (#449)
CI / toolchain (push) Waiting to run
CI / fmt (push) Waiting to run
CI / install-scripts (push) Waiting to run
CI / crates-macos-latest (push) Blocked by required conditions
CI / crates-ubuntu-latest (push) Blocked by required conditions
CI / crates-windows-latest (push) Blocked by required conditions
CI / doc (push) Blocked by required conditions
CI / deny (push) Blocked by required conditions
Install.sh / build (macos-latest) (push) Waiting to run
Install.sh / build (ubuntu-latest) (push) Waiting to run
Site / build-site (push) Waiting to run

* build(remotefs): migrate to version 1

* fix(remote): preserve POSIX paths on Windows
This commit is contained in:
Christian Visintin
2026-09-16 19:32:06 +02:00
committed by GitHub
parent 74c91c747c
commit 4120311bf6
20 changed files with 1760 additions and 1406 deletions
Generated
+619 -802
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -46,7 +46,7 @@ bytesize = "2"
cbc = { version = "0.2", features = ["alloc"] }
chrono = "0.4"
content_inspector = "0.2"
dirs = "6"
dirs = "7"
edit = "0.1"
filetime = "0.2"
keyring-core = "1"
@@ -59,13 +59,13 @@ nucleo = "0.5"
open = "5"
rand = "0.10"
regex = "1"
remotefs = "0.3"
remotefs-aws-s3 = "0.4"
remotefs-gcs = "0.1"
remotefs-kube = "0.4"
remotefs-smb = { version = "0.5", default-features = false, optional = true, features = ["find", "pavao"] }
remotefs-ssh = { version = "0.9", default-features = false, features = ["russh"] }
remotefs-webdav = "0.2"
remotefs = "1"
remotefs-aws-s3 = { version = "1", features = ["tokio"] }
remotefs-gcs = { version = "1", features = ["tokio"] }
remotefs-kube = { version = "1", features = ["tokio"] }
remotefs-smb = { version = "1", default-features = false, optional = true, features = ["find", "pavao"] }
remotefs-ssh = { version = "1", default-features = false, features = ["russh"] }
remotefs-webdav = { version = "1", features = ["tokio"] }
rpassword = "7"
self_update = { version = "0.42", default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] }
semver = "1"
@@ -76,7 +76,7 @@ simplelog = "0.12"
ssh2-config = "0.8"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["rt"] }
tokio = { version = "1", features = ["rt-multi-thread"] }
toml = "1"
tui-realm-stdlib = "4"
tui-term = "0.3"
@@ -89,11 +89,11 @@ wildmatch = "2"
dbus-secret-service-keyring-store = { version = "1", features = ["crypto-rust", "vendored"] }
[target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "0.4", features = ["native-tls", "native-tls-vendored"] }
remotefs-ftp = { version = "1", features = ["native-tls", "native-tls-vendored"] }
uzers = "0.12"
[target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "0.4", features = ["native-tls"] }
remotefs-ftp = { version = "1", features = ["native-tls"] }
[target."cfg(target_os = \"macos\")".dependencies]
apple-native-keyring-store = { version = "1", features = ["keychain"] }
+34 -46
View File
@@ -565,20 +565,18 @@ mod tests {
let explorer: FileExplorer = FileExplorer::default();
// Create fs entry
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
let entry = File::new(
"/bar.txt",
Metadata::default()
.accessed(t)
.created(t)
.modified(t)
.file_type(FileType::File)
.uid(0)
.gid(0)
.mode(UnixPex::from(0o644))
.size(8192),
);
#[cfg(posix)]
assert_eq!(
explorer.fmt_file(&entry),
@@ -690,47 +688,37 @@ mod tests {
fn make_fs_entry(name: &str, is_dir: bool) -> File {
let t: SystemTime = SystemTime::now();
let metadata = Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: if is_dir {
let metadata = Metadata::default()
.accessed(t)
.created(t)
.modified(t)
.file_type(if is_dir {
FileType::Directory
} else {
FileType::File
},
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: 64,
};
File {
path: PathBuf::from(name),
metadata,
}
})
.gid(0)
.uid(0)
.mode(UnixPex::from(if is_dir { 0o755 } else { 0o644 }))
.size(64);
File::new(name, metadata)
}
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> File {
let t: SystemTime = SystemTime::now();
let metadata = Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: if is_dir {
let metadata = Metadata::default()
.accessed(t)
.created(t)
.modified(t)
.file_type(if is_dir {
FileType::Directory
} else {
FileType::File
},
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: size as u64,
};
File {
path: PathBuf::from(name),
metadata,
}
})
.gid(0)
.uid(0)
.mode(UnixPex::from(if is_dir { 0o755 } else { 0o644 }))
.size(size as u64);
File::new(name, metadata)
}
}
+231 -255
View File
@@ -363,8 +363,10 @@ impl Formatter {
) -> String {
if fsentry.is_file() {
// Get byte size
let size: ByteSize = ByteSize(fsentry.metadata().size);
let mut fmt = size.display().si().to_string();
let mut fmt = fsentry.metadata().size.map_or_else(
|| String::from("?"),
|size| ByteSize(size).display().si().to_string(),
);
// pad with up to len 10
let pad = 10usize.saturating_sub(fmt.len());
for _ in 0..pad {
@@ -534,7 +536,6 @@ impl Formatter {
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::time::SystemTime;
use pretty_assertions::assert_eq;
@@ -542,26 +543,61 @@ mod tests {
use super::*;
#[expect(
clippy::too_many_arguments,
reason = "The fixture parameters mirror the optional remotefs metadata fields."
)]
fn fixture(
path: &str,
file_type: FileType,
size: Option<u64>,
symlink: Option<&str>,
uid: Option<u32>,
gid: Option<u32>,
mode: Option<UnixPex>,
timestamp: Option<SystemTime>,
) -> File {
let mut metadata = Metadata::default().file_type(file_type);
if let Some(size) = size {
metadata = metadata.size(size);
}
if let Some(symlink) = symlink {
metadata = metadata.symlink(symlink);
}
if let Some(uid) = uid {
metadata = metadata.uid(uid);
}
if let Some(gid) = gid {
metadata = metadata.gid(gid);
}
if let Some(mode) = mode {
metadata = metadata.mode(mode);
}
if let Some(timestamp) = timestamp {
metadata = metadata
.accessed(timestamp)
.created(timestamp)
.modified(timestamp);
}
File::new(path, metadata)
}
#[test]
fn test_fs_explorer_formatter_callchain() {
// Make a dummy formatter
let dummy_formatter: Formatter = Formatter::new("");
// Make a dummy entry
let t: SystemTime = SystemTime::now();
let dummy_entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
let dummy_entry = fixture(
"/bar.txt",
FileType::File,
Some(8192),
None,
Some(0),
Some(0),
Some(UnixPex::from(0o644)),
Some(t),
);
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
assert!(callchain.next_block.is_none());
@@ -589,20 +625,16 @@ mod tests {
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
let entry = fixture(
"/bar.txt",
FileType::File,
Some(8192),
None,
Some(0),
Some(0),
Some(UnixPex::from(0o644)),
Some(t),
);
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
@@ -620,20 +652,16 @@ mod tests {
)
);
// Elide name
let entry = File {
path: PathBuf::from("/piroparoporoperoperupupu.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
let entry = fixture(
"/piroparoporoperoperupupu.txt",
FileType::File,
Some(8192),
None,
Some(0),
Some(0),
Some(UnixPex::from(0o644)),
Some(t),
);
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
@@ -651,20 +679,16 @@ mod tests {
)
);
// No pex
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: None,
},
};
let entry = fixture(
"/bar.txt",
FileType::File,
Some(8192),
None,
Some(0),
Some(0),
None,
Some(t),
);
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
@@ -682,20 +706,16 @@ mod tests {
)
);
// No user
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
};
let entry = fixture(
"/bar.txt",
FileType::File,
Some(8192),
None,
None,
Some(0),
None,
Some(t),
);
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
@@ -720,20 +740,16 @@ mod tests {
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Directory,
size: 4096,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o755)),
},
};
let entry = fixture(
"/home/cvisintin/projects",
FileType::Directory,
Some(4096),
None,
Some(0),
Some(0),
Some(UnixPex::from(0o755)),
Some(t),
);
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
@@ -751,20 +767,16 @@ mod tests {
)
);
// No pex, no user
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Directory,
size: 4096,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
};
let entry = fixture(
"/home/cvisintin/projects",
FileType::Directory,
Some(4096),
None,
None,
Some(0),
None,
Some(t),
);
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
@@ -790,20 +802,16 @@ mod tests {
);
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 4096,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
};
let entry = fixture(
"/home/cvisintin/projects",
FileType::Symlink,
Some(4096),
Some("project.info"),
None,
None,
Some(UnixPex::from(0o755)),
Some(t),
);
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -814,20 +822,16 @@ mod tests {
)
);
// Directory without symlink
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Directory,
size: 4096,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
};
let entry = fixture(
"/home/cvisintin/projects",
FileType::Directory,
Some(4096),
None,
None,
None,
Some(UnixPex::from(0o755)),
Some(t),
);
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -838,20 +842,16 @@ mod tests {
)
);
// File with symlink
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
let entry = fixture(
"/bar.txt",
FileType::Symlink,
Some(8192),
Some("project.info"),
None,
None,
Some(UnixPex::from(0o644)),
Some(t),
);
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -862,20 +862,16 @@ mod tests {
)
);
// File without symlink
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
let entry = fixture(
"/bar.txt",
FileType::File,
Some(8192),
None,
None,
None,
Some(UnixPex::from(0o644)),
Some(t),
);
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -891,20 +887,16 @@ mod tests {
#[cfg(posix)]
fn should_fmt_path() {
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
let entry = fixture(
"/tmp/a/b/c/bar.txt",
FileType::Symlink,
Some(8192),
Some("project.info"),
None,
None,
Some(UnixPex::from(0o644)),
Some(t),
);
let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!(
formatter.fmt(&entry).as_str(),
@@ -923,20 +915,16 @@ mod tests {
#[cfg(posix)]
fn should_fmt_utf8_path() {
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/tmp/a/b/c/россия"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
let entry = fixture(
"/tmp/a/b/c/россия",
FileType::Symlink,
Some(8192),
Some("project.info"),
None,
None,
Some(UnixPex::from(0o644)),
Some(t),
);
let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!(
formatter.fmt(&entry).as_str(),
@@ -948,100 +936,88 @@ mod tests {
#[test]
fn should_fmt_short_ascii_name() {
let entry = File {
path: PathBuf::from("/tmp/foo.txt"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let entry = fixture(
"/tmp/foo.txt",
FileType::File,
Some(8192),
None,
None,
None,
None,
None,
);
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "foo.txt ");
}
#[test]
fn should_fmt_exceeding_length_ascii_name() {
let entry = File {
path: PathBuf::from("/tmp/christian-visintin.txt"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let entry = fixture(
"/tmp/christian-visintin.txt",
FileType::File,
Some(8192),
None,
None,
None,
None,
None,
);
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "christi…");
}
#[test]
fn should_fmt_short_utf8_name() {
let entry = File {
path: PathBuf::from("/tmp/россия"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let entry = fixture(
"/tmp/россия",
FileType::File,
Some(8192),
None,
None,
None,
None,
None,
);
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "россия ");
}
#[test]
fn should_fmt_long_utf8_name() {
let entry = File {
path: PathBuf::from("/tmp/喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let entry = fixture(
"/tmp/喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵",
FileType::File,
Some(8192),
None,
None,
None,
None,
None,
);
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…");
}
#[test]
fn should_fmt_unknown_file_size() {
let entry = File::new("/tmp/unknown", Metadata::default());
let formatter = Formatter::new("{SIZE}");
assert_eq!(formatter.fmt(&entry), "? ");
}
#[test]
fn should_ignore_unknown_formatter_keys() {
let entry = File {
path: PathBuf::from("/tmp/foo.txt"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let entry = fixture(
"/tmp/foo.txt",
FileType::File,
Some(8192),
None,
None,
None,
None,
None,
);
let formatter: Formatter = Formatter::new("before {UNKNOWN:12} after {NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "before after foo.txt ");
+1
View File
@@ -5,6 +5,7 @@
mod host_bridge_builder;
pub mod params;
mod remotefs_builder;
mod wrapper;
// -- export types
pub use host_bridge_builder::HostBridgeBuilder;
+52 -43
View File
@@ -3,14 +3,13 @@
//! Remotefs client builder
use std::path::PathBuf;
use std::sync::Arc;
use remotefs::RemoteFs;
use remotefs_aws_s3::AwsS3Fs;
use remotefs_ftp::FtpFs;
use remotefs_gcs::credentials::service_account;
use remotefs_gcs::{GoogleCloudStorageCredentials, GoogleCloudStorageFs};
use remotefs_kube::KubeMultiPodFs as KubeFs;
use remotefs_kube::KubeMultiPodFs;
#[cfg(smb_unix)]
use remotefs_smb::{
PavaoSmbCredentials as SmbCredentials, PavaoSmbFs as SmbFs, PavaoSmbOptions as SmbOptions,
@@ -19,8 +18,7 @@ use remotefs_smb::{
#[cfg(smb_windows)]
use remotefs_smb::{WNetSmbCredentials as SmbCredentials, WNetSmbFs as SmbFs};
use remotefs_ssh::{
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
SshConfigParseRule, SshOpts,
NoCheckServerKey, RusshScpFs, RusshSftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts,
};
use remotefs_webdav::WebDAVFs;
@@ -31,6 +29,7 @@ use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams
#[cfg(smb)]
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, SmbParams};
use super::params::{KubeProtocolParams, WebDAVProtocolParams};
use super::wrapper::RuntimeRemoteFs;
use super::{FileTransferProtocol, ProtocolParams};
use crate::system::config_client::ConfigClient;
use crate::system::sshkey_storage::SshKeyStorage;
@@ -72,7 +71,7 @@ impl RemoteFsBuilder {
Ok(Box::new(Self::smb_client(params)?))
}
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
Ok(Box::new(Self::webdav_client(params)))
Ok(Box::new(Self::webdav_client(params)?))
}
(protocol, params) => {
error!("Invalid params for protocol '{:?}'", protocol);
@@ -84,16 +83,11 @@ impl RemoteFsBuilder {
}
/// Build aws s3 client from parameters
fn aws_s3_client(params: AwsS3Params) -> Result<AwsS3Fs, String> {
let rt = Arc::new(
tokio::runtime::Builder::new_current_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
);
let mut client =
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
fn aws_s3_client(params: AwsS3Params) -> Result<RuntimeRemoteFs, String> {
let runtime = Self::tokio_runtime()?;
let mut client = AwsS3Fs::new(params.bucket_name).new_path_style(params.new_path_style);
if let Some(region) = params.region {
client = client.region(region);
}
@@ -115,14 +109,15 @@ impl RemoteFsBuilder {
if let Some(session_token) = params.session_token {
client = client.session_token(session_token);
}
Ok(client)
let client = client.into_blocking(runtime.handle().clone());
Ok(RuntimeRemoteFs::new(client, runtime))
}
/// Build a Google Cloud Storage client from parameters.
fn gcs_client(params: GoogleCloudStorageParams) -> Result<GoogleCloudStorageFs, String> {
fn gcs_client(params: GoogleCloudStorageParams) -> Result<RuntimeRemoteFs, String> {
let runtime = Self::tokio_runtime()?;
let mut client = match params.service_account_key {
None => GoogleCloudStorageFs::new(params.bucket_name, &runtime),
None => GoogleCloudStorageFs::new(params.bucket_name),
Some(path) => {
let raw = std::fs::read_to_string(&path).map_err(|error| {
format!("Unable to read GCS service-account file '{path}': {error}")
@@ -140,12 +135,12 @@ impl RemoteFsBuilder {
GoogleCloudStorageFs::with_credentials(
params.bucket_name,
GoogleCloudStorageCredentials::custom(credentials),
&runtime,
)
}
};
client = client.endpoint(params.endpoint);
Ok(client)
let client = client.into_blocking(runtime.handle().clone());
Ok(RuntimeRemoteFs::new(client, runtime))
}
/// Build ftp client from parameters
@@ -164,34 +159,42 @@ impl RemoteFsBuilder {
}
/// Build kube client
fn kube_client(params: KubeProtocolParams) -> Result<KubeFs, String> {
let rt = Self::tokio_runtime()?;
let kube_fs = KubeFs::new(&rt);
if let Some(config) = params.config() {
Ok(kube_fs.config(config))
fn kube_client(params: KubeProtocolParams) -> Result<RuntimeRemoteFs, String> {
let runtime = Self::tokio_runtime()?;
let kube_fs = if let Some(config) = params.config() {
KubeMultiPodFs::new().config(config)
} else {
Ok(kube_fs)
}
KubeMultiPodFs::new()
};
let client = kube_fs.into_blocking(runtime.handle().clone());
Ok(RuntimeRemoteFs::new(client, runtime))
}
/// Build scp client
fn scp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> Result<ScpFs<SshSession<NoCheckServerKey>>, String> {
) -> Result<RuntimeRemoteFs, String> {
let opts = Self::build_ssh_opts(params, config_client);
let rt = Self::tokio_runtime()?;
Ok(ScpFs::russh(opts, rt))
let runtime = Self::tokio_runtime()?;
let client =
RusshScpFs::<NoCheckServerKey>::new(opts).into_blocking(runtime.handle().clone());
Ok(RuntimeRemoteFs::new(client, runtime))
}
/// Build sftp client
fn sftp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> Result<SftpFs<SshSession<NoCheckServerKey>>, String> {
) -> Result<RuntimeRemoteFs, String> {
let opts = Self::build_ssh_opts(params, config_client);
let rt = Self::tokio_runtime()?;
Ok(SftpFs::russh(opts, rt))
let runtime = Self::tokio_runtime()?;
let client =
RusshSftpFs::<NoCheckServerKey>::new(opts).into_blocking(runtime.handle().clone());
Ok(RuntimeRemoteFs::new(client, runtime))
}
/// Maps the user-facing SMB family to inclusive remotefs dialect bounds.
@@ -251,8 +254,16 @@ impl RemoteFsBuilder {
Ok(SmbFs::new(credentials))
}
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
WebDAVFs::new(&params.username, &params.password, &params.uri)
fn webdav_client(params: WebDAVProtocolParams) -> Result<RuntimeRemoteFs, String> {
let runtime = Self::tokio_runtime()?;
let client = WebDAVFs::new(
&params.uri,
remotefs_webdav::Auth::basic(params.username, params.password),
)
.map_err(|e| format!("failed to create WebDAV client: {e}"))?;
let client = client.into_blocking(runtime.handle().clone());
Ok(RuntimeRemoteFs::new(client, runtime))
}
/// Build ssh options from generic protocol params and client configuration
@@ -288,14 +299,12 @@ impl RemoteFsBuilder {
}
/// Create tokio runtime to run async code for remotefs
fn tokio_runtime() -> Result<Arc<tokio::runtime::Runtime>, String> {
Ok(Arc::new(
tokio::runtime::Builder::new_current_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
))
fn tokio_runtime() -> Result<tokio::runtime::Runtime, String> {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|e| format!("Unable to create tokio runtime: {e}"))
}
}
+154
View File
@@ -0,0 +1,154 @@
//! Runtime-owning wrapper for asynchronous remote filesystem adapters.
use std::io::{Read, Write};
use std::path::Path;
use remotefs::fs::{
Capabilities, ExecOutput, ReadOptions, ReadStream, SetMetadata, UnixPex, WriteOptions,
WriteStream,
};
use remotefs::{File, RemoteFs, RemoteResult};
use tokio::runtime::Runtime;
pub(super) struct RuntimeRemoteFs {
// Fields are dropped in declaration order, so the client cannot outlive its runtime.
remote: Box<dyn RemoteFs>,
_runtime: Runtime,
}
impl RuntimeRemoteFs {
pub(super) fn new(remote: impl RemoteFs + 'static, runtime: Runtime) -> Self {
Self {
remote: Box::new(remote),
_runtime: runtime,
}
}
}
impl RemoteFs for RuntimeRemoteFs {
fn connect(&mut self) -> RemoteResult<()> {
self.remote.connect()
}
fn disconnect(&mut self) -> RemoteResult<()> {
self.remote.disconnect()
}
fn is_connected(&self) -> bool {
self.remote.is_connected()
}
fn capabilities(&self) -> Capabilities {
self.remote.capabilities()
}
fn list_dir(&self, path: &Path) -> RemoteResult<Vec<File>> {
self.remote.list_dir(path)
}
fn stat(&self, path: &Path) -> RemoteResult<File> {
self.remote.stat(path)
}
fn exists(&self, path: &Path) -> RemoteResult<bool> {
self.remote.exists(path)
}
fn set_metadata(&self, path: &Path, metadata: &SetMetadata) -> RemoteResult<()> {
self.remote.set_metadata(path, metadata)
}
fn create_dir(&self, path: &Path, mode: Option<UnixPex>) -> RemoteResult<()> {
self.remote.create_dir(path, mode)
}
fn remove_file(&self, path: &Path) -> RemoteResult<()> {
self.remote.remove_file(path)
}
fn remove_dir(&self, path: &Path) -> RemoteResult<()> {
self.remote.remove_dir(path)
}
fn remove_dir_all(&self, path: &Path) -> RemoteResult<()> {
self.remote.remove_dir_all(path)
}
fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
self.remote.rename(src, dest)
}
fn copy(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
self.remote.copy(src, dest)
}
fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
self.remote.symlink(path, target)
}
fn open(&self, path: &Path, opts: &ReadOptions) -> RemoteResult<ReadStream> {
self.remote.open(path, opts)
}
fn create(&self, path: &Path, opts: &WriteOptions) -> RemoteResult<WriteStream> {
self.remote.create(path, opts)
}
fn append(&self, path: &Path, opts: &WriteOptions) -> RemoteResult<WriteStream> {
self.remote.append(path, opts)
}
fn read_file(
&self,
path: &Path,
opts: &ReadOptions,
dest: &mut (dyn Write + Send),
) -> RemoteResult<u64> {
self.remote.read_file(path, opts, dest)
}
fn write_file(
&self,
path: &Path,
opts: &WriteOptions,
src: &mut (dyn Read + Send),
) -> RemoteResult<u64> {
self.remote.write_file(path, opts, src)
}
fn append_file(
&self,
path: &Path,
opts: &WriteOptions,
src: &mut (dyn Read + Send),
) -> RemoteResult<u64> {
self.remote.append_file(path, opts, src)
}
fn exec(&self, cmd: &str) -> RemoteResult<ExecOutput> {
self.remote.exec(cmd)
}
}
#[cfg(test)]
mod tests {
use remotefs_ftp::FtpFs;
use super::*;
#[test]
fn runtime_remote_fs_keeps_runtime_alive() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.unwrap();
let handle = runtime.handle().clone();
let remote = RuntimeRemoteFs::new(FtpFs::new("127.0.0.1", 21), runtime);
let task = handle.spawn(async { 42 });
assert_eq!(handle.block_on(task).unwrap(), 42);
drop(remote);
}
}
+3 -1
View File
@@ -11,7 +11,8 @@ use std::path::{Path, PathBuf};
use thiserror::Error;
// Locals
pub use self::bridge::HostBridge;
#[doc(inline)]
pub use self::bridge::{HostBridge, HostReader, HostWriter};
pub use self::localhost::Localhost;
pub use self::remote_bridged::RemoteBridged;
@@ -48,6 +49,7 @@ pub enum HostErrorType {
/// HostError is a wrapper for the error type and the exact io error
#[derive(Debug, Error)]
pub struct HostError {
#[source]
pub error: HostErrorType,
ioerr: Option<std::io::Error>,
path: Option<PathBuf>,
+213 -8
View File
@@ -3,7 +3,8 @@
//! Defines the host abstraction used to expose localhost and bridged remote
//! filesystems through a shared interface.
use std::io::{Read, Write};
use std::fmt;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use remotefs::File;
@@ -11,6 +12,111 @@ use remotefs::fs::{Metadata, UnixPex};
use super::HostResult;
enum HostReaderInner {
Io(Box<dyn Read + Send>),
Remote(remotefs::fs::ReadStream),
}
/// An owned host reader that optionally retains a remote transfer finalizer.
pub struct HostReader(HostReaderInner);
impl HostReader {
pub(crate) fn io<T>(reader: T) -> Self
where
T: Read + Send + 'static,
{
Self(HostReaderInner::Io(Box::new(reader)))
}
pub(crate) fn remote(reader: remotefs::fs::ReadStream) -> Self {
Self(HostReaderInner::Remote(reader))
}
/// Completes the remote read and consumes this reader.
///
/// # Errors
///
/// Returns the remote stream finalization error, if the reader is backed
/// by a remote stream.
pub fn finish(self) -> super::HostResult<()> {
match self.0 {
HostReaderInner::Io(_) => Ok(()),
HostReaderInner::Remote(reader) => reader.finish().map_err(Into::into),
}
}
}
impl fmt::Debug for HostReader {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("HostReader").finish_non_exhaustive()
}
}
impl Read for HostReader {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
match &mut self.0 {
HostReaderInner::Io(reader) => reader.read(buffer),
HostReaderInner::Remote(reader) => reader.read(buffer),
}
}
}
enum HostWriterInner {
Io(Box<dyn Write + Send>),
Remote(remotefs::fs::WriteStream),
}
/// An owned host writer that optionally retains a remote transfer finalizer.
pub struct HostWriter(HostWriterInner);
impl HostWriter {
pub(crate) fn io<T>(writer: T) -> Self
where
T: Write + Send + 'static,
{
Self(HostWriterInner::Io(Box::new(writer)))
}
pub(crate) fn remote(writer: remotefs::fs::WriteStream) -> Self {
Self(HostWriterInner::Remote(writer))
}
/// Completes the remote write and consumes this writer.
///
/// # Errors
///
/// Returns the remote stream finalization error, if the writer is backed
/// by a remote stream.
pub fn finish(self) -> super::HostResult<()> {
match self.0 {
HostWriterInner::Io(_) => Ok(()),
HostWriterInner::Remote(writer) => writer.finish().map_err(Into::into),
}
}
}
impl fmt::Debug for HostWriter {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("HostWriter").finish_non_exhaustive()
}
}
impl Write for HostWriter {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
match &mut self.0 {
HostWriterInner::Io(writer) => writer.write(buffer),
HostWriterInner::Remote(writer) => writer.write(buffer),
}
}
fn flush(&mut self) -> io::Result<()> {
match &mut self.0 {
HostWriterInner::Io(writer) => writer.flush(),
HostWriterInner::Remote(writer) => writer.flush(),
}
}
}
/// Trait to bridge a remote filesystem to the host filesystem
///
/// In case of `Localhost` this should be effortless, while for remote hosts this should
@@ -75,15 +181,114 @@ pub trait HostBridge {
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>;
/// Open file for reading
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>>;
fn open_file(&mut self, file: &Path) -> HostResult<HostReader>;
/// Open file for writing
fn create_file(
&mut self,
file: &Path,
metadata: &Metadata,
) -> HostResult<Box<dyn Write + Send>>;
fn create_file(&mut self, file: &Path, metadata: &Metadata) -> HostResult<HostWriter>;
/// Finalize write operation
fn finalize_write(&mut self, writer: Box<dyn Write + Send>) -> HostResult<()>;
fn finalize_write(&mut self, writer: HostWriter) -> HostResult<()>;
}
#[cfg(test)]
mod test {
use std::io::Cursor;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use remotefs::fs::{ReadStream, RemoteRead, RemoteWrite, WriteStream};
use super::*;
struct TrackedReader {
reader: Cursor<Vec<u8>>,
finishes: Arc<AtomicUsize>,
}
impl Read for TrackedReader {
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
self.reader.read(buffer)
}
}
impl RemoteRead for TrackedReader {
fn finish(self: Box<Self>) -> remotefs::RemoteResult<()> {
self.finishes.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
struct TrackedWriter {
writer: Cursor<Vec<u8>>,
finishes: Arc<AtomicUsize>,
}
impl Write for TrackedWriter {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
self.writer.write(buffer)
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
impl RemoteWrite for TrackedWriter {
fn finish(self: Box<Self>) -> remotefs::RemoteResult<()> {
self.finishes.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
#[test]
fn remote_reader_finishes_exactly_once_when_consumed() {
let finishes = Arc::new(AtomicUsize::new(0));
let reader = HostReader::remote(ReadStream::new(TrackedReader {
reader: Cursor::new(Vec::new()),
finishes: finishes.clone(),
}));
reader.finish().unwrap();
assert_eq!(finishes.load(Ordering::SeqCst), 1);
}
#[test]
fn remote_writer_finishes_exactly_once_when_consumed() {
let finishes = Arc::new(AtomicUsize::new(0));
let writer = HostWriter::remote(WriteStream::new(TrackedWriter {
writer: Cursor::new(Vec::new()),
finishes: finishes.clone(),
}));
writer.finish().unwrap();
assert_eq!(finishes.load(Ordering::SeqCst), 1);
}
#[test]
fn unfinished_remote_reader_is_dropped_without_finalizing() {
let finishes = Arc::new(AtomicUsize::new(0));
let reader = HostReader::remote(ReadStream::new(TrackedReader {
reader: Cursor::new(Vec::new()),
finishes: finishes.clone(),
}));
drop(reader);
assert_eq!(finishes.load(Ordering::SeqCst), 0);
}
#[test]
fn unfinished_remote_writer_is_dropped_without_finalizing() {
let finishes = Arc::new(AtomicUsize::new(0));
let writer = HostWriter::remote(WriteStream::new(TrackedWriter {
writer: Cursor::new(Vec::new()),
finishes: finishes.clone(),
}));
drop(writer);
assert_eq!(finishes.load(Ordering::SeqCst), 0);
}
}
+12 -14
View File
@@ -4,7 +4,7 @@
//! filesystem.
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::io::Write;
#[cfg(posix)]
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
@@ -13,7 +13,7 @@ use filetime::FileTime;
use remotefs::File;
use remotefs::fs::{FileType, Metadata, UnixPex};
use super::{HostBridge, HostResult};
use super::{HostBridge, HostReader, HostResult, HostWriter};
use crate::host::{HostError, HostErrorType};
use crate::utils::path;
@@ -368,7 +368,7 @@ impl HostBridge for Localhost {
metadata.file_type = FileType::Symlink;
}
// Match dir / file
Ok(File { path, metadata })
Ok(File::new(path, metadata))
}
fn exists(&mut self, path: &Path) -> HostResult<bool> {
@@ -520,7 +520,7 @@ impl HostBridge for Localhost {
Err(HostError::from(HostErrorType::NotImplemented))
}
fn open_file(&mut self, file: &std::path::Path) -> HostResult<Box<dyn Read + Send>> {
fn open_file(&mut self, file: &std::path::Path) -> HostResult<HostReader> {
let file: PathBuf = self.to_path(file);
info!("Opening file {} for read", file.display());
if !self.exists(file.as_path())? {
@@ -537,7 +537,7 @@ impl HostBridge for Localhost {
.write(false)
.open(file.as_path())
{
Ok(f) => Ok(Box::new(f)),
Ok(f) => Ok(HostReader::io(f)),
Err(err) => {
error!("Could not open file for read: {}", err);
Err(HostError::new(
@@ -549,11 +549,7 @@ impl HostBridge for Localhost {
}
}
fn create_file(
&mut self,
file: &Path,
_metadata: &Metadata,
) -> HostResult<Box<dyn Write + Send>> {
fn create_file(&mut self, file: &Path, _metadata: &Metadata) -> HostResult<HostWriter> {
let file: PathBuf = self.to_path(file);
info!("Opening file {} for write", file.display());
match OpenOptions::new()
@@ -562,7 +558,7 @@ impl HostBridge for Localhost {
.truncate(true)
.open(file.as_path())
{
Ok(f) => Ok(Box::new(f)),
Ok(f) => Ok(HostWriter::io(f)),
Err(err) => {
error!("Failed to open file: {}", err);
match self.exists(file.as_path())? {
@@ -581,9 +577,11 @@ impl HostBridge for Localhost {
}
}
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
// no-op
Ok(())
fn finalize_write(&mut self, mut writer: HostWriter) -> HostResult<()> {
writer.flush().map_err(|error| {
HostError::new(HostErrorType::FileNotAccessible, Some(error), Path::new(""))
})?;
writer.finish()
}
}
+238 -89
View File
@@ -5,19 +5,19 @@
mod temp_mapped_file;
use std::io::{Read, Write};
use std::io::Write;
use std::path::{Component, Path, PathBuf};
use remotefs::fs::{Metadata, UnixPex};
use remotefs::fs::{Capabilities, Metadata, ReadOptions, SetMetadata, UnixPex, WriteOptions};
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
use self::temp_mapped_file::TempMappedFile;
use super::{HostBridge, HostError, HostResult};
use super::{HostBridge, HostError, HostErrorType, HostReader, HostResult, HostWriter};
use crate::utils::path::normalize;
struct WriteStreamOp {
path: PathBuf,
metadata: Metadata,
options: WriteOptions,
tempfile: TempMappedFile,
}
@@ -25,30 +25,81 @@ struct WriteStreamOp {
pub struct RemoteBridged {
/// Remote fs client
remote: Box<dyn RemoteFs>,
/// Consumer-owned remote working directory used to resolve relative paths
wrkdir: PathBuf,
/// 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>> {
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<HostReader> {
let mut temp_file = TempMappedFile::new()?;
self.remote
.open_file(file, Box::new(temp_file.clone()))
.read_file(file, &ReadOptions::default(), &mut temp_file)
.map_err(HostError::from)?;
// Sync changes
temp_file.sync()?;
// now return as read
Ok(Box::new(temp_file))
Ok(HostReader::io(temp_file))
}
}
fn resolve_remote_path(wrkdir: &Path, target: &Path) -> HostResult<PathBuf> {
let resolved = if remotefs::path::ensure_absolute(target).is_ok() {
target.to_path_buf()
} else {
wrkdir.join(target)
};
remotefs::path::ensure_absolute(&resolved).map_err(HostError::from)?;
Ok(resolved)
}
fn write_options(metadata: &Metadata) -> WriteOptions {
let mut options = WriteOptions::default();
if let Some(size) = metadata.size {
options = options.size_hint(size);
}
if let Some(mode) = metadata.mode {
options = options.mode(mode);
}
if let Some(modified) = metadata.modified {
options = options.modified(modified);
}
options
}
fn set_metadata_options(metadata: &Metadata) -> SetMetadata {
let mut options = SetMetadata::default();
if let Some(mode) = metadata.mode {
options = options.mode(mode);
}
if let Some(uid) = metadata.uid {
options = options.uid(uid);
}
if let Some(gid) = metadata.gid {
options = options.gid(gid);
}
if let Some(accessed) = metadata.accessed {
options = options.accessed(accessed);
}
if let Some(modified) = metadata.modified {
options = options.modified(modified);
}
options
}
fn map_io_error(error: std::io::Error) -> HostError {
HostError::new(HostErrorType::FileNotAccessible, Some(error), Path::new(""))
}
impl From<Box<dyn RemoteFs>> for RemoteBridged {
fn from(remote: Box<dyn RemoteFs>) -> Self {
RemoteBridged {
remote,
wrkdir: PathBuf::from("/"),
write_stream_op: None,
}
}
@@ -56,7 +107,7 @@ impl From<Box<dyn RemoteFs>> for RemoteBridged {
impl HostBridge for RemoteBridged {
fn connect(&mut self) -> HostResult<()> {
self.remote.connect().map(|_| ()).map_err(HostError::from)
self.remote.connect().map_err(HostError::from)
}
fn disconnect(&mut self) -> HostResult<()> {
@@ -73,71 +124,85 @@ impl HostBridge for RemoteBridged {
fn pwd(&mut self) -> HostResult<PathBuf> {
debug!("Getting working directory");
self.remote.pwd().map_err(HostError::from)
Ok(self.wrkdir.clone())
}
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)
let new_dir = resolve_remote_path(&self.wrkdir, new_dir)?;
let entry = self.remote.stat(&new_dir).map_err(HostError::from)?;
if !entry.is_dir() {
return Err(HostError::from(RemoteError::new(RemoteErrorType::BadFile)));
}
self.wrkdir = new_dir;
Ok(self.wrkdir.clone())
}
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)),
let path = resolve_remote_path(&self.wrkdir, dir_name)?;
match self.remote.create_dir(&path, Some(UnixPex::from(0o755))) {
Ok(()) => Ok(()),
Err(error) if ignore_existing && error.kind() == RemoteErrorType::AlreadyExists => {
Ok(())
}
Err(error) => Err(HostError::from(error)),
}
}
fn remove(&mut self, entry: &File) -> HostResult<()> {
debug!("Removing {:?}", entry.path());
let path = resolve_remote_path(&self.wrkdir, entry.path())?;
if entry.is_dir() {
self.remote
.remove_dir_all(entry.path())
.map_err(HostError::from)
self.remote.remove_dir_all(&path).map_err(HostError::from)
} else {
self.remote
.remove_file(entry.path())
.map_err(HostError::from)
self.remote.remove_file(&path).map_err(HostError::from)
}
}
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> {
debug!("Renaming {:?} to {:?}", entry.path(), dst_path);
let source = resolve_remote_path(&self.wrkdir, entry.path())?;
let destination = resolve_remote_path(&self.wrkdir, dst_path)?;
self.remote
.mov(entry.path(), dst_path)
.rename(&source, &destination)
.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)
let source = resolve_remote_path(&self.wrkdir, entry.path())?;
let destination = resolve_remote_path(&self.wrkdir, dst)?;
self.remote
.copy(&source, &destination)
.map_err(HostError::from)
}
fn stat(&mut self, path: &Path) -> HostResult<File> {
debug!("Statting {:?}", path);
self.remote.stat(path).map_err(HostError::from)
let path = resolve_remote_path(&self.wrkdir, 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)
let path = resolve_remote_path(&self.wrkdir, 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))
let path = resolve_remote_path(&self.wrkdir, 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);
let path = resolve_remote_path(&self.wrkdir, path)?;
let options = set_metadata_options(metadata);
self.remote
.setstat(path, metadata.clone())
.set_metadata(&path, &options)
.map_err(HostError::from)
}
@@ -145,78 +210,108 @@ impl HostBridge for RemoteBridged {
debug!("Executing command: {}", cmd);
self.remote
.exec(cmd)
.map(|(_, stdout)| stdout)
.map(|output| output.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)
let path = resolve_remote_path(&self.wrkdir, src)?;
let target = resolve_remote_path(&self.wrkdir, dst)?;
self.remote.symlink(&path, &target).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)
let path = resolve_remote_path(&self.wrkdir, path)?;
self.remote
.set_metadata(&path, &SetMetadata::default().mode(pex))
.map_err(HostError::from)
}
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))
fn open_file(&mut self, file: &Path) -> HostResult<HostReader> {
let path = resolve_remote_path(&self.wrkdir, file)?;
if self
.remote
.capabilities()
.contains(Capabilities::STREAM_READ)
{
match self.remote.open(&path, &ReadOptions::default()) {
Ok(stream) => Ok(HostReader::remote(stream)),
Err(error) if error.kind() == RemoteErrorType::UnsupportedFeature => {
self.open_file_from_temp(&path)
}
Err(error) => Err(HostError::from(error)),
}
Err(e) => Err(HostError::from(e)),
} else {
self.open_file_from_temp(&path)
}
}
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
fn create_file(&mut self, file: &Path, metadata: &Metadata) -> HostResult<HostWriter> {
let path = resolve_remote_path(&self.wrkdir, file)?;
let options = write_options(metadata);
self.write_stream_op = None;
if self
.remote
.capabilities()
.contains(Capabilities::STREAM_WRITE)
{
match self.remote.create(&path, &options) {
Ok(stream) => Ok(HostWriter::remote(stream)),
Err(error)
if matches!(
error.kind(),
RemoteErrorType::SizeRequired | RemoteErrorType::UnsupportedFeature
) =>
{
self.create_file_from_temp(path, options)
}
Err(error) => Err(HostError::from(error)),
}
} else {
self.create_file_from_temp(path, options)
}
}
fn finalize_write(&mut self, mut writer: HostWriter) -> HostResult<()> {
writer.flush().map_err(map_io_error)?;
writer.finish()?;
if let Some(WriteStreamOp {
path,
metadata,
mut options,
mut tempfile,
}) = self.write_stream_op.take()
{
// sync
tempfile.sync()?;
options = options.size_hint(tempfile.len()?);
// write file
self.remote
.create_file(&path, &metadata, Box::new(tempfile))?;
.write_file(&path, &options, &mut tempfile)
.map_err(HostError::from)?;
}
Ok(())
}
}
impl RemoteBridged {
fn create_file_from_temp(
&mut self,
path: PathBuf,
options: WriteOptions,
) -> HostResult<HostWriter> {
let tempfile = TempMappedFile::new()?;
self.write_stream_op = Some(WriteStreamOp {
path,
options,
tempfile: tempfile.clone(),
});
Ok(HostWriter::io(tempfile))
}
}
/// Drop entries that refer to the directory being listed.
///
/// Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
@@ -236,31 +331,85 @@ fn filter_self_refs(path: &Path, entries: Vec<File>) -> Vec<File> {
#[cfg(test)]
mod test {
use std::path::PathBuf;
use std::time::SystemTime;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use pretty_assertions::assert_eq;
use remotefs::fs::{FileType, Metadata};
use remotefs::fs::{FileType, Metadata, UnixPex};
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,
},
File::new(
path,
Metadata::default()
.accessed(UNIX_EPOCH)
.created(UNIX_EPOCH)
.modified(UNIX_EPOCH)
.file_type(file_type)
.size(0),
)
}
#[test]
fn resolves_relative_paths_from_consumer_working_directory() {
assert_eq!(
resolve_remote_path(Path::new("/home/user"), Path::new("docs/report.txt")).unwrap(),
Path::new("/home/user/docs/report.txt")
);
}
#[test]
fn preserves_absolute_remote_paths() {
for path in ["/srv/data", r"C:\data\file", r"\\server\share\file"] {
assert_eq!(
resolve_remote_path(Path::new("/ignored"), Path::new(path)).unwrap(),
Path::new(path)
);
}
}
#[test]
fn maps_metadata_to_write_options() {
let modified = SystemTime::UNIX_EPOCH;
let mode = UnixPex::from(0o640);
let options = write_options(&Metadata::default().size(42).mode(mode).modified(modified));
assert_eq!(options.size_hint, Some(42));
assert_eq!(options.mode, Some(mode));
assert_eq!(options.modified, Some(modified));
}
#[test]
fn maps_settable_metadata_fields() {
let accessed = SystemTime::UNIX_EPOCH;
let modified = accessed + std::time::Duration::from_secs(1);
let mode = UnixPex::from(0o640);
let options = set_metadata_options(
&Metadata::default()
.accessed(accessed)
.gid(20)
.mode(mode)
.modified(modified)
.uid(10),
);
assert_eq!(options.accessed, Some(accessed));
assert_eq!(options.gid, Some(20));
assert_eq!(options.mode, Some(mode));
assert_eq!(options.modified, Some(modified));
assert_eq!(options.uid, Some(10));
}
#[test]
fn default_metadata_maps_to_empty_options() {
assert_eq!(write_options(&Metadata::default()), Default::default());
assert_eq!(
set_metadata_options(&Metadata::default()),
Default::default()
);
}
#[test]
fn filter_self_refs_drops_entry_matching_listed_dir() {
let entries = vec![
@@ -82,6 +82,19 @@ impl TempMappedFile {
Ok(())
}
/// Returns the number of bytes currently stored in the temporary file.
pub fn len(&self) -> HostResult<u64> {
std::fs::metadata(self.tempfile.path())
.map(|metadata| metadata.len())
.map_err(|error| {
HostError::new(
HostErrorType::FileNotAccessible,
Some(error),
self.tempfile.path(),
)
})
}
fn write_hnd(&mut self) -> io::Result<FileHandle<'_>> {
let mut lock = self.lock_handle()?;
if lock.is_none() {
+16 -9
View File
@@ -3,7 +3,7 @@
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
use std::fs::OpenOptions;
use std::io::Read;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
@@ -107,10 +107,12 @@ impl FileTransferActivity {
}
};
let new_file_size = match std::io::copy(&mut reader, &mut writer) {
Err(err) => return Err(format!("Could not write file: {err}")),
Ok(size) => size,
};
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
return Err(format!("Could not write file: {err}"));
}
reader
.finish()
.map_err(|err| format!("Could not finish file read: {err}"))?;
// edit file
@@ -126,10 +128,11 @@ impl FileTransferActivity {
};
let mut writer = match self.browser.local_pane_mut().fs.create_file(
entry.path(),
&Metadata {
size: new_file_size,
..Default::default()
},
&Metadata::default().size(
std::fs::metadata(tempfile.as_path())
.map_err(|err| format!("Could not stat edited file: {err}"))?
.len(),
),
) {
Ok(writer) => writer,
Err(err) => {
@@ -141,6 +144,10 @@ impl FileTransferActivity {
return Err(format!("Could not write file: {err}"));
}
writer
.flush()
.map_err(|err| format!("Could not write file: {err}"))?;
self.browser
.local_pane_mut()
.fs
@@ -2,6 +2,12 @@ use remotefs::File;
use super::{FileTransferActivity, LogLevel};
fn aggregate_sizes(sizes: impl IntoIterator<Item = Option<u64>>) -> Option<u64> {
sizes
.into_iter()
.try_fold(0_u64, |total, size| total.checked_add(size?))
}
impl FileTransferActivity {
/// Calculate and display the total size of the selected file(s) via the active tab's pane.
pub(crate) fn action_get_file_size(&mut self) {
@@ -11,17 +17,23 @@ impl FileTransferActivity {
let total_size = self.get_files_size(files);
self.umount_wait();
self.mount_info(format!(
"Total file size: {size}",
size = bytesize::ByteSize::b(total_size)
));
let message = total_size.map_or_else(
|| String::from("Total file size: Unknown"),
|size| {
format!(
"Total file size: {size}",
size = bytesize::ByteSize::b(size)
)
},
);
self.mount_info(message);
}
fn get_files_size(&mut self, files: Vec<File>) -> u64 {
files.into_iter().map(|f| self.get_file_size(f)).sum()
fn get_files_size(&mut self, files: Vec<File>) -> Option<u64> {
aggregate_sizes(files.into_iter().map(|file| self.get_file_size(file)))
}
fn get_file_size(&mut self, file: File) -> u64 {
fn get_file_size(&mut self, file: File) -> Option<u64> {
if let Some(symlink) = &file.metadata().symlink {
match self.browser.fs_pane_mut().fs.stat(symlink) {
Ok(stat) => stat.metadata().size,
@@ -33,12 +45,12 @@ impl FileTransferActivity {
path = symlink.display(),
),
);
0
None
}
}
} else if file.is_dir() {
match self.browser.fs_pane_mut().fs.list_dir(&file.path) {
Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(),
Ok(list) => self.get_files_size(list),
Err(err) => {
self.log(
LogLevel::Error,
@@ -47,7 +59,7 @@ impl FileTransferActivity {
path = file.path.display(),
),
);
0
None
}
}
} else {
@@ -55,3 +67,14 @@ impl FileTransferActivity {
}
}
}
#[cfg(test)]
mod test {
use super::aggregate_sizes;
#[test]
fn aggregates_only_known_sizes() {
assert_eq!(aggregate_sizes([Some(2), Some(3)]), Some(5));
assert_eq!(aggregate_sizes([Some(2), None, Some(3)]), None);
}
}
@@ -28,7 +28,7 @@ impl FileTransferActivity {
.browser
.fs_pane_mut()
.fs
.create_file(file_path.as_path(), &Metadata::default())
.create_file(file_path.as_path(), &Metadata::default().size(0))
{
Ok(f) => f,
Err(err) => {
@@ -131,6 +131,14 @@ impl FileTransferActivity {
return;
}
if let Err(err) = reader.finish() {
self.log(
LogLevel::Error,
format!("Failed to finish bridged file read: {err}"),
);
return;
}
if tmpfile.exists() {
self.open_path_with(tmpfile.as_path(), open_with);
}
@@ -43,11 +43,10 @@ impl FileInfoPopup {
.add_col(SpanStatic::from("File type: "))
.add_col(SpanStatic::raw(filetype.clone()).fg(Color::LightGreen));
}
let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size);
texts
.add_row()
.add_col(SpanStatic::from("Size: "))
.add_col(SpanStatic::raw(format!("{bsize} ({size})")).fg(Color::Cyan));
.add_col(SpanStatic::raw(format_file_size(file)).fg(Color::Cyan));
let atime: String = fmt_time(
file.metadata().accessed.unwrap_or(UNIX_EPOCH),
"%b %d %Y %H:%M:%S",
@@ -117,6 +116,13 @@ impl FileInfoPopup {
}
}
fn format_file_size(file: &File) -> String {
match file.metadata().size {
Some(size) => format!("{bsize} ({size})", bsize = ByteSize(size), size = size),
None => String::from("Unknown"),
}
}
impl AppComponent<Msg, NoUserEvent> for FileInfoPopup {
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev {
@@ -128,3 +134,17 @@ impl AppComponent<Msg, NoUserEvent> for FileInfoPopup {
}
}
}
#[cfg(test)]
mod test {
use remotefs::fs::Metadata;
use super::*;
#[test]
fn formats_unknown_file_size_as_unknown() {
let file = File::new("/tmp/unknown", Metadata::default());
assert_eq!(format_file_size(&file), "Unknown");
}
}
+45 -23
View File
@@ -16,7 +16,7 @@ use bytesize::ByteSize;
pub struct TransferProgress {
files_total: usize,
files_completed: usize,
cur_file_size: usize,
cur_file_size: Option<usize>,
cur_file_written: usize,
total_bytes_written: usize,
pub(crate) started: Instant,
@@ -27,7 +27,7 @@ impl Default for TransferProgress {
Self {
files_total: 0,
files_completed: 0,
cur_file_size: 0,
cur_file_size: None,
cur_file_written: 0,
total_bytes_written: 0,
started: Instant::now(),
@@ -41,13 +41,15 @@ impl fmt::Display for TransferProgress {
0 => String::from("--:--"),
seconds => format!("{:02}:{:02}", seconds / 60, seconds % 60),
};
let total = self.cur_file_size.map_or_else(
|| String::from("?"),
|size| ByteSize(size as u64).to_string(),
);
write!(
f,
"{} / {} — {:.1}% — ETA {} ({}/s)",
"{} / {total} — {:.1}% — ETA {eta} ({}/s)",
ByteSize(self.cur_file_written as u64),
ByteSize(self.cur_file_size as u64),
self.calc_partial_progress() * 100.0,
eta,
ByteSize(self.calc_bytes_per_second()),
)
}
@@ -58,15 +60,15 @@ impl TransferProgress {
pub fn init(&mut self, files_total: usize) {
self.files_total = files_total;
self.files_completed = 0;
self.cur_file_size = 0;
self.cur_file_size = None;
self.cur_file_written = 0;
self.total_bytes_written = 0;
self.started = Instant::now();
}
/// Begin a new file with a known size.
pub fn start_file(&mut self, size: usize) {
self.cur_file_size = size;
/// Begin a new file with an optional advertised size.
pub fn start_file(&mut self, size: Option<u64>) {
self.cur_file_size = size.and_then(|size| usize::try_from(size).ok());
self.cur_file_written = 0;
}
@@ -83,7 +85,7 @@ impl TransferProgress {
/// fraction in [`Self::calc_full_progress`].
pub fn finish_file(&mut self) {
self.files_completed += 1;
self.cur_file_size = 0;
self.cur_file_size = None;
self.cur_file_written = 0;
}
@@ -92,25 +94,26 @@ impl TransferProgress {
self.files_completed += 1;
}
/// Fraction of the current file written (0.0..=1.0). Zero-byte file => 1.0.
/// Fraction of the current file written (0.0..=1.0).
pub fn calc_partial_progress(&self) -> f64 {
if self.cur_file_size == 0 {
return 1.0;
match self.cur_file_size {
None => 0.0,
Some(0) => 1.0,
Some(size) => (self.cur_file_written as f64 / size as f64).min(1.0),
}
(self.cur_file_written as f64 / self.cur_file_size as f64).min(1.0)
}
/// Overall progress (0.0..=1.0): file-weighted with intra-file interpolation.
///
/// The current file only contributes a fraction while it is genuinely in
/// progress (`cur_file_size > 0` and not all files completed). A finished
/// file clears `cur_file_size` (see [`Self::finish_file`]) so it is counted
/// progress (a known size and not all files completed). A finished file
/// clears `cur_file_size` (see [`Self::finish_file`]) so it is counted
/// exactly once via `files_completed`.
pub fn calc_full_progress(&self) -> f64 {
if self.files_total == 0 {
return 0.0;
}
let cur_fraction = if self.cur_file_size == 0 || self.files_completed >= self.files_total {
let cur_fraction = if self.files_completed >= self.files_total {
0.0
} else {
self.calc_partial_progress()
@@ -234,7 +237,7 @@ mod test {
assert!(progress.is_single_file());
assert_eq!(progress.calc_full_progress(), 0.0);
progress.start_file(1024);
progress.start_file(Some(1024));
assert_eq!(progress.calc_partial_progress(), 0.0);
assert_eq!(progress.calc_full_progress(), 0.0);
@@ -258,13 +261,13 @@ mod test {
assert!(!progress.is_single_file());
// File 1 fully transferred => full ≈ 0.25
progress.start_file(1000);
progress.start_file(Some(1000));
progress.add_bytes(1000);
progress.finish_file();
assert!((progress.calc_full_progress() - 0.25).abs() < 1e-9);
// File 2 half transferred => partial ≈ 0.5, full ≈ 0.375
progress.start_file(1000);
progress.start_file(Some(1000));
progress.add_bytes(500);
assert!((progress.calc_partial_progress() - 0.5).abs() < 1e-9);
assert!((progress.calc_full_progress() - 0.375).abs() < 1e-9);
@@ -276,7 +279,7 @@ mod test {
progress.init(2);
// One file actually transferred.
progress.start_file(100);
progress.start_file(Some(100));
progress.add_bytes(100);
progress.finish_file();
@@ -294,15 +297,34 @@ mod test {
let mut progress = TransferProgress::default();
progress.init(1);
progress.start_file(0);
progress.start_file(Some(0));
assert!((progress.calc_partial_progress() - 1.0).abs() < 1e-9);
}
#[test]
fn test_transfer_progress_unknown_size() {
let mut progress = TransferProgress::default();
progress.init(1);
progress.start_file(None);
progress.add_bytes(256);
assert_eq!(progress.calc_partial_progress(), 0.0);
assert_eq!(progress.calc_full_progress(), 0.0);
assert_eq!(progress.calc_eta(), 0);
assert_eq!(progress.total_bytes_written(), 256);
assert!(progress.to_string().contains("?"));
assert!(!progress.to_string().contains("0 B"));
progress.finish_file();
assert_eq!(progress.files_completed(), 1);
assert_eq!(progress.calc_full_progress(), 1.0);
}
#[test]
fn test_transfer_progress_timing() {
let mut progress = TransferProgress::default();
progress.init(1);
progress.start_file(1024);
progress.start_file(Some(1024));
progress.started = progress
.started
@@ -10,7 +10,7 @@ use bytesize::ByteSize;
use remotefs::fs::File;
use thiserror::Error;
use crate::host::HostError;
use crate::host::{HostError, HostReader, HostWriter};
use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel};
use crate::utils::fmt::fmt_millis;
@@ -457,8 +457,8 @@ impl FileTransferActivity {
host: &File,
remote: &Path,
file_name: String,
mut reader: Box<dyn Read + Send>,
mut writer: Box<dyn Write + Send>,
mut reader: HostReader,
mut writer: HostWriter,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size = self
@@ -467,18 +467,16 @@ impl FileTransferActivity {
.fs
.stat(host.path())
.map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().size as usize)?;
.map(|x| x.metadata().size)?;
// Init transfer
self.transfer.progress.start_file(file_size);
let file_started = Instant::now();
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_redraw: Instant = Instant::now();
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Read until EOF or until the transfer is aborted.
while !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
@@ -494,33 +492,17 @@ impl FileTransferActivity {
}
// Read till you can
let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::HostIoError(err));
}
};
let bytes_read = reader
.read(&mut buffer)
.map_err(TransferErrorReason::HostIoError)?;
if bytes_read == 0 {
break;
}
writer
.write_all(&buffer[..bytes_read])
.map_err(TransferErrorReason::RemoteIoError)?;
// Increase progress
self.transfer.progress.add_bytes(delta);
self.transfer.progress.add_bytes(bytes_read);
// Redraw at most every 100ms to keep UI responsive for large files
if last_redraw.elapsed().as_millis() >= 100 {
self.update_progress_bar(format!("Uploading \"{file_name}\""));
@@ -528,12 +510,14 @@ impl FileTransferActivity {
last_redraw = Instant::now();
}
}
// Finalize stream
handle_remote_finalize_result(self.browser.remote_pane_mut().fs.finalize_write(writer))?;
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
writer.flush().map_err(TransferErrorReason::RemoteIoError)?;
reader.finish().map_err(TransferErrorReason::HostError)?;
// Finalize stream
handle_remote_finalize_result(self.browser.remote_pane_mut().fs.finalize_write(writer))?;
// set stat
if let Err(err) = self
.browser
@@ -812,21 +796,17 @@ impl FileTransferActivity {
host_bridge: &Path,
remote: &File,
file_name: String,
mut reader: Box<dyn Read + Send>,
mut writer: Box<dyn Write + Send>,
mut reader: HostReader,
mut writer: HostWriter,
) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer
.progress
.start_file(remote.metadata.size as usize);
self.transfer.progress.start_file(remote.metadata.size);
let file_started = Instant::now();
// Write host_bridge file
let mut last_redraw: Instant = Instant::now();
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() {
// Read until EOF or until the transfer is aborted.
while !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
@@ -842,31 +822,17 @@ impl FileTransferActivity {
}
// Read till you can
let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::HostIoError(err));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
let bytes_read = reader
.read(&mut buffer)
.map_err(TransferErrorReason::RemoteIoError)?;
if bytes_read == 0 {
break;
}
writer
.write_all(&buffer[..bytes_read])
.map_err(TransferErrorReason::HostIoError)?;
// Set progress
self.transfer.progress.add_bytes(delta);
self.transfer.progress.add_bytes(bytes_read);
// Redraw at most every 100ms to keep UI responsive for large files
if last_redraw.elapsed().as_millis() >= 100 {
self.update_progress_bar(format!("Downloading \"{file_name}\""));
@@ -879,6 +845,10 @@ impl FileTransferActivity {
return Err(TransferErrorReason::Abrupted);
}
writer.flush().map_err(TransferErrorReason::HostIoError)?;
reader
.finish()
.map_err(TransferErrorReason::RemoteHostError)?;
// Finalize write
self.browser
.local_pane_mut()
@@ -953,25 +923,20 @@ mod worklist_test {
// equal (`File` derives `PartialEq` over its full metadata, timestamps
// included), allowing direct equality assertions against the worklist.
let t = SystemTime::UNIX_EPOCH;
let metadata = Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: if is_dir {
let metadata = Metadata::default()
.accessed(t)
.created(t)
.modified(t)
.file_type(if is_dir {
FileType::Directory
} else {
FileType::File
},
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: 64,
};
File {
path: PathBuf::from(path),
metadata,
}
})
.gid(0)
.uid(0)
.mode(UnixPex::from(if is_dir { 0o755 } else { 0o644 }))
.size(64);
File::new(path, metadata)
}
#[test]
+4 -7
View File
@@ -14,10 +14,7 @@ pub fn create_sample_file_entry() -> (File, NamedTempFile) {
// Write
let tmpfile = create_sample_file();
(
File {
path: tmpfile.path().to_path_buf(),
metadata: Metadata::default(),
},
File::new(tmpfile.path().to_path_buf(), Metadata::default()),
tmpfile,
)
}
@@ -58,14 +55,14 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
/// Create a File at specified path
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> File {
let path: PathBuf = path.as_ref().to_path_buf();
File {
File::new(
path,
metadata: Metadata::default().file_type(if is_dir {
Metadata::default().file_type(if is_dir {
FileType::Directory
} else {
FileType::File
}),
}
)
}
/// Open a file with two handlers, the first is to read, the second is to write