feat: add sync files\folders
This commit is contained in:
Generated
+130
-1
@@ -156,6 +156,9 @@ name = "bitflags"
|
|||||||
version = "2.13.1"
|
version = "2.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake2"
|
name = "blake2"
|
||||||
@@ -379,6 +382,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-bigint"
|
name = "crypto-bigint"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
@@ -464,6 +473,20 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "6.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -797,6 +820,18 @@ version = "0.3.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "group"
|
name = "group"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -808,6 +843,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -902,7 +943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -979,6 +1020,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.34"
|
version = "0.4.34"
|
||||||
@@ -1162,6 +1212,19 @@ dependencies = [
|
|||||||
"windows-strings",
|
"windows-strings",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "password-hash"
|
name = "password-hash"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -1336,6 +1399,15 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -1474,6 +1546,26 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "russh-sftp"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9de67aace74530a29086db0671fa200c470a58eb380081f28ad512ffb0c5356b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"dashmap",
|
||||||
|
"gloo-timers",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_bytes",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "russh-util"
|
name = "russh-util"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -1520,6 +1612,12 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scrypt"
|
name = "scrypt"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -1562,6 +1660,16 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_bytes"
|
||||||
|
version = "0.11.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -1672,6 +1780,12 @@ version = "0.4.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
@@ -1851,6 +1965,20 @@ dependencies = [
|
|||||||
"syn 3.0.3",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.4+spec-1.1.0"
|
version = "1.1.4+spec-1.1.0"
|
||||||
@@ -2234,6 +2362,7 @@ dependencies = [
|
|||||||
"colored",
|
"colored",
|
||||||
"home",
|
"home",
|
||||||
"russh",
|
"russh",
|
||||||
|
"russh-sftp",
|
||||||
"serde",
|
"serde",
|
||||||
"ssh2-config",
|
"ssh2-config",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ clap = { version = "4.6.6", features = ["derive"] }
|
|||||||
colored = "3.1.1"
|
colored = "3.1.1"
|
||||||
home = "0.5.12"
|
home = "0.5.12"
|
||||||
russh = { version = "0.62.7", default-features = false, features = ["ring", "rsa", "flate2"] }
|
russh = { version = "0.62.7", default-features = false, features = ["ring", "rsa", "flate2"] }
|
||||||
|
russh-sftp = "2.4.0"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
ssh2-config = "0.7.2"
|
ssh2-config = "0.7.2"
|
||||||
tokio = { version = "1.53.1", features = ["rt", "time", "io-util"] }
|
tokio = { version = "1.53.1", features = ["rt", "time", "io-util"] }
|
||||||
|
|||||||
@@ -109,10 +109,12 @@ commands не пустые для секций, которые присутст
|
|||||||
|
|
||||||
### Этап 4 — SFTP-синхронизация
|
### Этап 4 — SFTP-синхронизация
|
||||||
|
|
||||||
1. src/sync.rs: открытие SFTP-сесси поверх установленной SSH-сессии.
|
1. src/sync.rs: открытие SFTP-сессии поверх установленной SSH-сессии
|
||||||
2. Одиночный файл: sftp.open/sendfile -> запись в target.
|
(subsystem sfp через russh + крейт russh-sftp, см. ADR-006).
|
||||||
3. Рекурсивная папка: walkdir по source; для каждой подпапки удалённый mkdir
|
2. Одиночный файл: remote create/truncate -> запись -> shutdown; если target —
|
||||||
с игнорированием ошибки "already exists" (аналог mkdir -p); файлы поверх.
|
существующий каталог, файл кладётся внутрь с локальным именем.
|
||||||
|
3. Рекурсивная папка: walkdir по source; для каждой подпапки mkdir если её ещё нет
|
||||||
|
(проверка metadata, аналог mkdir -p); файлы поверх.
|
||||||
4. Лог: `[SFTP] transferring dist/ -> /var/www/node-app ... Done!`,
|
4. Лог: `[SFTP] transferring dist/ -> /var/www/node-app ... Done!`,
|
||||||
счётчик файлов/байт.
|
счётчик файлов/байт.
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -2,6 +2,7 @@ mod cli;
|
|||||||
mod config;
|
mod config;
|
||||||
mod local;
|
mod local;
|
||||||
mod ssh;
|
mod ssh;
|
||||||
|
mod sync;
|
||||||
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
@@ -93,10 +94,10 @@ fn run(cli: &cli::Cli) -> Result<()> {
|
|||||||
session = Some(ssh::connect(&resolved)?);
|
session = Some(ssh::connect(&resolved)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !project.sync.is_empty() {
|
|
||||||
println!("stage 4 not implemented yet: SFTP file transfer");
|
|
||||||
}
|
|
||||||
if let Some(session) = session.as_mut() {
|
if let Some(session) = session.as_mut() {
|
||||||
|
for rule in &project.sync {
|
||||||
|
sync::upload(session, &rule.source, &rule.target.to_string_lossy())?;
|
||||||
|
}
|
||||||
for cmd in remote_cmds {
|
for cmd in remote_cmds {
|
||||||
session.exec(cmd)?;
|
session.exec(cmd)?;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -121,7 +121,7 @@ fn query_ssh_config(alias: &str) -> HostConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClientHandler {
|
pub(crate) struct ClientHandler {
|
||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,13 @@ pub struct Session {
|
|||||||
handle: Handle<ClientHandler>,
|
handle: Handle<ClientHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime() -> &'static tokio::runtime::Runtime {
|
impl Session {
|
||||||
|
pub(crate) fn handle(&mut self) -> &mut Handle<ClientHandler> {
|
||||||
|
&mut self.handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn runtime() -> &'static tokio::runtime::Runtime {
|
||||||
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
||||||
RT.get_or_init(|| {
|
RT.get_or_init(|| {
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
|||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use russh_sftp::client::SftpSession;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::ssh::{Session, runtime};
|
||||||
|
|
||||||
|
pub struct Stats {
|
||||||
|
pub files: u64,
|
||||||
|
pub bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Stats {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} file(s), {}", self.files, human_size(self.bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn human_size(bytes: u64) -> String {
|
||||||
|
const KIB: f64 = 1024.0;
|
||||||
|
const MIB: f64 = 1024.0 * 1024.0;
|
||||||
|
let b = bytes as f64;
|
||||||
|
if b >= MIB {
|
||||||
|
format!("{:.1} MiB", b / MIB)
|
||||||
|
} else if b >= KIB {
|
||||||
|
format!("{:.1} KiB", b / KIB)
|
||||||
|
} else {
|
||||||
|
format!("{bytes} B")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload(session: &mut Session, source: &Path, target: &str) -> Result<Stats> {
|
||||||
|
println!("[SFTP] transferring {} -> {} ...", source.display(), target);
|
||||||
|
let stats = runtime().block_on(async {
|
||||||
|
if source.is_dir() {
|
||||||
|
upload_dir(session, source, target).await
|
||||||
|
} else if source.is_file() {
|
||||||
|
upload_file(session, source, target).await
|
||||||
|
} else {
|
||||||
|
bail!("sync source '{}' does not exist", source.display());
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
println!("[SFTP] ... Done! ({stats})");
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_sftp(session: &mut Session) -> Result<SftpSession> {
|
||||||
|
let channel = session
|
||||||
|
.handle()
|
||||||
|
.channel_open_session()
|
||||||
|
.await
|
||||||
|
.context("failed to open SFTP channel")?;
|
||||||
|
channel
|
||||||
|
.request_subsystem(true, "sftp")
|
||||||
|
.await
|
||||||
|
.context("remote host has no SFTP subsystem")?;
|
||||||
|
let sftp = SftpSession::new(channel.into_stream())
|
||||||
|
.await
|
||||||
|
.context("SFTP session init failed")?;
|
||||||
|
Ok(sftp)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn mkdir_if_missing(sftp: &SftpSession, path: &str) -> Result<()> {
|
||||||
|
match sftp.metadata(path).await {
|
||||||
|
Ok(meta) if meta.is_dir() => return Ok(()),
|
||||||
|
Ok(_) => bail!("remote path exists but is not a directory: {path}"),
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
sftp.create_dir(path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("cannot create remote directory {path}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_remote(base: &str, rel: &str) -> String {
|
||||||
|
let rel = rel.replace('\\', "/");
|
||||||
|
if rel.is_empty() {
|
||||||
|
base.trim_end_matches('/').to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", base.trim_end_matches('/'), rel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_dir(session: &mut Session, source: &Path, target: &str) -> Result<Stats> {
|
||||||
|
let sftp = open_sftp(session).await?;
|
||||||
|
mkdir_if_missing(&sftp, target).await?;
|
||||||
|
|
||||||
|
let mut stats = Stats { files: 0, bytes: 0 };
|
||||||
|
for entry in WalkDir::new(source) {
|
||||||
|
let entry = entry.with_context(|| format!("cannot read local dir {}", source.display()))?;
|
||||||
|
let rel = entry
|
||||||
|
.path()
|
||||||
|
.strip_prefix(source)
|
||||||
|
.expect("walkdir entry is inside source");
|
||||||
|
let remote_path = join_remote(target, &rel.to_string_lossy());
|
||||||
|
|
||||||
|
if entry.file_type().is_dir() {
|
||||||
|
if !rel.as_os_str().is_empty() {
|
||||||
|
mkdir_if_missing(&sftp, &remote_path).await?;
|
||||||
|
}
|
||||||
|
} else if entry.file_type().is_file() {
|
||||||
|
write_remote_file(&sftp, entry.path(), &remote_path).await?;
|
||||||
|
stats.files += 1;
|
||||||
|
stats.bytes += std::fs::metadata(entry.path())
|
||||||
|
.map(|m| m.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_file(session: &mut Session, source: &Path, target: &str) -> Result<Stats> {
|
||||||
|
let sftp = open_sftp(session).await?;
|
||||||
|
let dest_is_dir = sftp
|
||||||
|
.metadata(target)
|
||||||
|
.await
|
||||||
|
.map(|meta| meta.is_dir())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let dest = if dest_is_dir {
|
||||||
|
let name = source
|
||||||
|
.file_name()
|
||||||
|
.with_context(|| format!("cannot determine file name of {}", source.display()))?;
|
||||||
|
join_remote(target, &name.to_string_lossy())
|
||||||
|
} else {
|
||||||
|
target.to_string()
|
||||||
|
};
|
||||||
|
write_remote_file(&sftp, source, &dest).await?;
|
||||||
|
let size = std::fs::metadata(source).map(|m| m.len()).unwrap_or(0);
|
||||||
|
Ok(Stats {
|
||||||
|
files: 1,
|
||||||
|
bytes: size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_remote_file(sftp: &SftpSession, local: &Path, remote_path: &str) -> Result<()> {
|
||||||
|
let data = std::fs::read(local).with_context(|| format!("cannot read {}", local.display()))?;
|
||||||
|
let mut remote_file = sftp
|
||||||
|
.create(remote_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("cannot create remote file {remote_path}"))?;
|
||||||
|
remote_file
|
||||||
|
.write_all(&data)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("upload interrupted: {remote_path}"))?;
|
||||||
|
remote_file
|
||||||
|
.shutdown()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("cannot finish writing {remote_path}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_remote_handles_slashes_and_edges() {
|
||||||
|
assert_eq!(join_remote("/var/www/pages", "dist"), "/var/www/pages/dist");
|
||||||
|
assert_eq!(
|
||||||
|
join_remote("/var/www/pages/", "a/b.js"),
|
||||||
|
"/var/www/pages/a/b.js"
|
||||||
|
);
|
||||||
|
assert_eq!(join_remote("/var/www/pages", ""), "/var/www/pages");
|
||||||
|
assert_eq!(
|
||||||
|
join_remote("/var/www/pages", "sub\\file.css"),
|
||||||
|
"/var/www/pages/sub/file.css"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn human_size_formats() {
|
||||||
|
assert_eq!(human_size(0), "0 B");
|
||||||
|
assert_eq!(human_size(512), "512 B");
|
||||||
|
assert_eq!(human_size(2048), "2.0 KiB");
|
||||||
|
assert_eq!(human_size(3 * 1024 * 1024), "3.0 MiB");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user