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