diff --git a/Cargo.lock b/Cargo.lock index d830bcb9..b1adef51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -636,6 +636,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.7.1" @@ -663,9 +669,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.94" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -766,6 +775,7 @@ dependencies = [ "indoc", "itertools", "lazy_static", + "libssh2-sys", "lru", "mutants", "nix", @@ -783,6 +793,7 @@ dependencies = [ "serde", "serde_json", "snap", + "ssh2", "strum", "strum_macros", "tempfile", @@ -797,6 +808,7 @@ dependencies = [ "unix_mode", "url", "uzers", + "whoami", ] [[package]] @@ -1092,7 +1104,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -1498,6 +1510,15 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -1524,6 +1545,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1542,6 +1572,32 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1715,7 +1771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "210b363fa6901c372f264fa32ef3710c0e86328901deaed31294fecfd51e848b" dependencies = [ "atty", - "parking_lot", + "parking_lot 0.12.1", "terminal_size 0.2.6", "yansi", ] @@ -1741,6 +1797,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "outref" version = "0.5.1" @@ -1764,6 +1832,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1771,7 +1850,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -1782,7 +1875,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -1815,6 +1908,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1987,6 +2086,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ea134c32fe12df286020949d57d052a90c4001f2dbec4c1c074f39bcb7fc8c" +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1996,6 +2104,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -2353,6 +2470,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2419,6 +2542,18 @@ dependencies = [ "der", ] +[[package]] +name = "ssh2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libssh2-sys", + "parking_lot 0.11.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2605,7 +2740,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2860,6 +2995,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2906,6 +3047,88 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.58", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall 0.5.7", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 1302f073..30efe016 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,19 @@ repository = "https://github.com/sourcefrog/conserve/" version = "24.8.0" rust-version = "1.78" +[features] +s3 = [ + "dep:aws-config", + "dep:aws-sdk-s3", + "dep:aws-types", + "dep:base64", + "dep:crc32c", + "dep:futures", + "dep:tokio", +] +s3-integration-test = ["s3"] +sftp = ["dep:ssh2", "dep:libssh2-sys"] + [[bin]] doc = false name = "conserve" @@ -34,6 +47,7 @@ globset = "0.4.5" hex = "0.4.2" itertools = "0.12" lazy_static = "1.4.0" +libssh2-sys = { version = "0.3.0", optional = true } lru = "0.12" mutants = "0.0.3" rayon = "1.3.0" @@ -43,12 +57,13 @@ semver = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" snap = "1.0.0" +ssh2 = { version = "0.9.4", optional = true } strum = "0.26" strum_macros = "0.26" tempfile = "3" thiserror = "1.0.19" thousands = "0.2.0" -time = { version = "0.3.28", features = [ +time = { version = "0.3.35", features = [ "local-offset", "macros", "serde", @@ -59,7 +74,7 @@ tracing = "0.1" tracing-appender = "0.2" unix_mode = "0.1" url = "2.2.2" -indoc = "2.0" +whoami = "1.5.2" [target.'cfg(unix)'.dependencies] uzers = "0.11" @@ -84,6 +99,7 @@ assert_cmd = "2.0" assert_fs = "1.0" cp_r = "0.5" dir-assert = "0.2" +indoc = "2.0" predicates = "3" pretty_assertions = "1.0" proptest = "1.0" @@ -92,21 +108,6 @@ rand = "0.8" rstest = { version = "0.19", default-features = false } tracing-test = { version = "0.2", features = ["no-env-filter"] } -[features] -default = [] -# blake2-rfc/simd_asm needs nightly, so it's no longer a feature here so that --all-features works on stable. -# blake2_simd_asm = ["blake2-rfc/simd_asm"] -s3 = [ - "dep:aws-config", - "dep:aws-sdk-s3", - "dep:aws-types", - "dep:base64", - "dep:crc32c", - "dep:futures", - "dep:tokio", -] -s3-integration-test = ["s3"] - [lib] doctest = false diff --git a/README.md b/README.md index f36ea296..ec351e21 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,12 @@ To install from a git checkout, run [rust]: https://rustup.rs/ -On nightly Rust only, and only on x86_64, you can enable a slight speed-up with +### Optional features - cargo +nightly install -f --path . --features blake2-rfc/simd_asm +The following options can be enabled with `--features`: + +* `s3`: support for storing backups in Amazon S3 (or compatible services) +* `sftp`: support for storing backups on SFTP servers, addressed with `sftp://` URLs ### Arch Linux diff --git a/doc/sftp.md b/doc/sftp.md new file mode 100644 index 00000000..2cf4a1f2 --- /dev/null +++ b/doc/sftp.md @@ -0,0 +1,12 @@ +# SFTP support + +Conserve can read and write archives over SFTP. + +To use this, just specify an SFTP URL, like `sftp://user@host/path`, for the archive location. + + conserve init sftp://user@host/path + conserve backup sftp://user@host/path ~ + +If no username is present in the URL, Conserve will use the current user's username. + +Currently, Conserve only supports agent authentication. diff --git a/src/stitch.rs b/src/stitch.rs index 68f1f9f9..dc936df1 100644 --- a/src/stitch.rs +++ b/src/stitch.rs @@ -175,10 +175,9 @@ fn previous_existing_band(archive: &Archive, mut band_id: BandId) -> Option crate::Result> { // Probably a Windows path with drive letter, like "c:/thing", not actually a URL. Ok(Arc::new(LocalTransport::new(Path::new(s)))) } + #[cfg(feature = "sftp")] + "sftp" => Ok(Arc::new(sftp::SftpTransport::new(&url)?)), other => Err(crate::Error::UrlScheme { scheme: other.to_owned(), }), @@ -177,22 +182,27 @@ pub enum ErrorKind { Other, } +impl From for ErrorKind { + fn from(kind: io::ErrorKind) -> Self { + match kind { + io::ErrorKind::NotFound => ErrorKind::NotFound, + io::ErrorKind::AlreadyExists => ErrorKind::AlreadyExists, + io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied, + _ => ErrorKind::Other, + } + } +} + impl Error { pub fn kind(&self) -> ErrorKind { self.kind } pub(self) fn io_error(path: &Path, source: io::Error) -> Error { - let kind = match source.kind() { - io::ErrorKind::NotFound => ErrorKind::NotFound, - io::ErrorKind::AlreadyExists => ErrorKind::AlreadyExists, - io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied, - _ => ErrorKind::Other, - }; Error { + kind: source.kind().into(), source: Some(Box::new(source)), path: Some(path.to_string_lossy().to_string()), - kind, } } diff --git a/src/transport/sftp.rs b/src/transport/sftp.rs new file mode 100644 index 00000000..3769e45b --- /dev/null +++ b/src/transport/sftp.rs @@ -0,0 +1,279 @@ +// Copyright 2022 Martin Pool + +//! Read/write archive over SFTP. + +use std::fmt; +use std::io::{self, Read, Write}; +use std::net::TcpStream; +use std::path::PathBuf; +use std::sync::Arc; + +use bytes::Bytes; +use tracing::{error, info, trace, warn}; +use url::Url; + +use crate::Kind; + +use super::{Error, ErrorKind, ListDir, Result, Transport}; + +/// Archive file I/O over SFTP. +#[derive(Clone)] +pub struct SftpTransport { + url: Url, + sftp: Arc, + base_path: PathBuf, +} + +impl SftpTransport { + pub fn new(url: &Url) -> Result { + assert_eq!(url.scheme(), "sftp"); + let addr = format!( + "{}:{}", + url.host_str().expect("url must have a host"), + url.port().unwrap_or(22) + ); + let tcp_stream = TcpStream::connect(addr).map_err(|err| { + error!(?err, ?url, "Error opening SSH TCP connection"); + io_error(err, url.as_ref()) + })?; + trace!("got tcp connection"); + let mut session = ssh2::Session::new().map_err(|err| { + error!(?err, "Error opening SSH session"); + ssh_error(err, url.as_ref()) + })?; + session.set_tcp_stream(tcp_stream); + session.handshake().map_err(|err| { + error!(?err, "Error in SSH handshake"); + ssh_error(err, url.as_ref()) + })?; + trace!( + "SSH hands shaken, banner: {}", + session.banner().unwrap_or("(none)") + ); + let username = match url.username() { + "" => { + trace!("Take default SSH username from environment"); + whoami::username() + } + u => u.to_owned(), + }; + session.userauth_agent(&username).map_err(|err| { + error!(?err, username, "Error in SSH user auth with agent"); + ssh_error(err, url.as_ref()) + })?; + trace!("Authenticated!"); + let sftp = session.sftp().map_err(|err| { + error!(?err, "Error opening SFTP session"); + ssh_error(err, url.as_ref()) + })?; + Ok(SftpTransport { + sftp: Arc::new(sftp), + url: url.clone(), + base_path: url.path().into(), + }) + } + + fn lstat(&self, path: &str) -> Result { + trace!("lstat {path}"); + self.sftp + .lstat(&self.base_path.join(path)) + .map_err(|err| ssh_error(err, path)) + } +} + +impl fmt::Debug for SftpTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SftpTransport") + .field("url", &self.url) + .finish() + } +} + +impl Transport for SftpTransport { + fn list_dir(&self, path: &str) -> Result { + let full_path = &self.base_path.join(path); + trace!("iter_dir_entries {:?}", full_path); + let mut files = Vec::new(); + let mut dirs = Vec::new(); + let mut dir = self.sftp.opendir(full_path).map_err(|err| { + error!(?err, ?full_path, "Error opening directory"); + ssh_error(err, full_path.to_string_lossy().as_ref()) + })?; + loop { + match dir.readdir() { + Ok((pathbuf, file_stat)) => { + let name = pathbuf.to_string_lossy().into(); + if name == "." || name == ".." { + continue; + } + trace!("read dir got name {}", name); + match file_stat.file_type().into() { + Kind::File => files.push(name), + Kind::Dir => dirs.push(name), + _ => (), + } + } + Err(err) if err.code() == ssh2::ErrorCode::Session(-16) => { + // Apparently there's no symbolic version for it, but this is the error + // code. + // + trace!("read dir end"); + break; + } + Err(err) => { + info!("SFTP error {:?}", err); + return Err(ssh_error(err, path)); + } + } + } + Ok(ListDir { files, dirs }) + } + + fn read_file(&self, path: &str) -> Result { + let full_path = self.base_path.join(path); + trace!("attempt open {}", full_path.display()); + let mut buf = Vec::with_capacity(2 << 20); + let mut file = self + .sftp + .open(&full_path) + .map_err(|err| ssh_error(err, path))?; + let len = file + .read_to_end(&mut buf) + .map_err(|err| io_error(err, path))?; + assert_eq!(len, buf.len()); + trace!("read {} bytes from {}", len, full_path.display()); + Ok(buf.into()) + } + + fn create_dir(&self, relpath: &str) -> Result<()> { + let full_path = self.base_path.join(relpath); + trace!("create_dir {:?}", full_path); + match self.sftp.mkdir(&full_path, 0o700) { + Ok(()) => Ok(()), + Err(err) if err.code() == ssh2::ErrorCode::SFTP(libssh2_sys::LIBSSH2_FX_FAILURE) => { + // openssh seems to say failure for "directory exists" :/ + Ok(()) + } + Err(err) => { + warn!(?err, ?relpath); + Err(ssh_error(err, relpath)) + } + } + } + + fn write_file(&self, relpath: &str, content: &[u8]) -> Result<()> { + let full_path = self.base_path.join(relpath); + trace!("write_file {:>9} bytes to {:?}", content.len(), full_path); + let mut file = self.sftp.create(&full_path).map_err(|err| { + warn!(?err, ?relpath, "sftp error creating file"); + ssh_error(err, relpath) + })?; + file.write_all(content).map_err(|err| { + warn!(?err, ?full_path, "sftp error writing file"); + io_error(err, relpath) + }) + } + + fn metadata(&self, relpath: &str) -> Result { + let full_path = self.base_path.join(relpath); + let stat = self.lstat(relpath)?; + trace!("metadata {full_path:?}"); + Ok(super::Metadata { + kind: stat.file_type().into(), + len: stat.size.unwrap_or_default(), + }) + } + + fn remove_file(&self, relpath: &str) -> Result<()> { + let full_path = self.base_path.join(relpath); + trace!("remove_file {full_path:?}"); + self.sftp + .unlink(&full_path) + .map_err(|err| ssh_error(err, relpath)) + } + + fn remove_dir_all(&self, path: &str) -> Result<()> { + trace!(?path, "SftpTransport::remove_dir_all"); + let mut dirs_to_walk = vec![path.to_owned()]; + let mut dirs_to_delete = vec![path.to_owned()]; + while let Some(dir) = dirs_to_walk.pop() { + trace!(?dir, "Walk down dir"); + let list = self.list_dir(&dir)?; + for file in list.files { + self.remove_file(&format!("{dir}/{file}"))?; + } + list.dirs + .iter() + .map(|subdir| format!("{dir}/{subdir}")) + .for_each(|p| { + dirs_to_delete.push(p.clone()); + dirs_to_walk.push(p) + }); + } + // Consume them in the reverse order discovered, so bottom up + for dir in dirs_to_delete.iter().rev() { + let full_path = self.base_path.join(dir); + trace!(?dir, "rmdir"); + self.sftp + .rmdir(&full_path) + .map_err(|err| ssh_error(err, dir))?; + } + Ok(()) + // let full_path = self.base_path.join(relpath); + // trace!("remove_dir {full_path:?}"); + // self.sftp.rmdir(&full_path).map_err(translate_error) + } + + fn sub_transport(&self, relpath: &str) -> Arc { + let base_path = self.base_path.join(relpath); + let mut url = self.url.clone(); + url.set_path(base_path.to_str().unwrap()); + Arc::new(SftpTransport { + url, + sftp: Arc::clone(&self.sftp), + base_path, + }) + } +} + +impl From for Kind { + fn from(kind: ssh2::FileType) -> Self { + use ssh2::FileType::*; + match kind { + RegularFile => Kind::File, + Directory => Kind::Dir, + Symlink => Kind::Symlink, + _ => Kind::Unknown, + } + } +} + +impl From for ErrorKind { + fn from(code: ssh2::ErrorCode) -> Self { + // Map other errors to io::Error that aren't handled by libssh. + // + // See https://github.com/alexcrichton/ssh2-rs/issues/244. + match code { + ssh2::ErrorCode::SFTP(libssh2_sys::LIBSSH2_FX_NO_SUCH_FILE) + | ssh2::ErrorCode::SFTP(libssh2_sys::LIBSSH2_FX_NO_SUCH_PATH) => ErrorKind::NotFound, + // TODO: Others + _ => ErrorKind::Other, + } + } +} + +fn ssh_error(source: ssh2::Error, path: &str) -> super::Error { + super::Error { + kind: source.code().into(), + source: Some(Box::new(source)), + path: Some(path.to_owned()), + } +} + +fn io_error(source: io::Error, path: &str) -> Error { + Error { + kind: source.kind().into(), + source: Some(Box::new(source)), + path: Some(path.to_owned()), + } +} diff --git a/tests/cli.rs b/tests/cli.rs index bd3babf1..df0a5756 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -31,7 +31,9 @@ use url::Url; use conserve::test_fixtures::{ScratchArchive, TreeFixture}; fn run_conserve() -> Command { - Command::cargo_bin("conserve").expect("locate conserve binary") + let mut command = Command::cargo_bin("conserve").expect("locate conserve binary"); + command.env_remove("RUST_LOG"); + command } #[test]