mirror of
https://github.com/veeso/termscp.git
synced 2026-09-17 09:35:57 -07:00
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
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:
committed by
GitHub
parent
74c91c747c
commit
4120311bf6
Generated
+619
-802
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -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
@@ -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
@@ -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 ");
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mod host_bridge_builder;
|
||||
pub mod params;
|
||||
mod remotefs_builder;
|
||||
mod wrapper;
|
||||
|
||||
// -- export types
|
||||
pub use host_bridge_builder::HostBridgeBuilder;
|
||||
|
||||
@@ -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(¶ms.username, ¶ms.password, ¶ms.uri)
|
||||
fn webdav_client(params: WebDAVProtocolParams) -> Result<RuntimeRemoteFs, String> {
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let client = WebDAVFs::new(
|
||||
¶ms.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}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user