use std::collections::{HashMap, HashSet};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use filetime::FileTime;
use crate::delta::{Signature, apply};
use crate::filter::Filter;
use crate::meta::{
canonical_root, check_relative, contained_target, set_mode, set_mtime, set_symlink_mtime,
};
use crate::net::proto::{Data, Msg, NetEntry, NetKind, NetOptions, Request};
use crate::net::transport::{recv_msg, send_msg};
use crate::plan::Action;
use crate::report::{Event, Reporter, RunPhase, Stats};
use crate::walk::{Entry, walk_controlled};
use crate::{Error, Result, RunControl};
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub fn run_receiver<C: Read + Write, R: Reporter>(
conn: &mut C,
root: &Path,
filter: &Filter,
options: NetOptions,
threads: usize,
control: &RunControl,
reporter: &R,
) -> Result<Stats> {
reporter.event(Event::Phase(RunPhase::Copying));
let root_canon = canonical_root(root)?;
let mut src_entries: Vec<NetEntry> = Vec::new();
loop {
control.checkpoint()?;
match recv_msg(conn)? {
Msg::Entry(e) => src_entries.push(e),
Msg::ListDone => break,
Msg::Error(e) => return Err(Error::Protocol(format!("peer error: {e}"))),
other => {
return Err(Error::Protocol(format!(
"receiver: expected entry/list-done, got {other:?}"
)));
}
}
}
let existing = walk_controlled(root, threads, filter, control)?;
let existing_map: HashMap<PathBuf, Entry> = existing
.iter()
.map(|e| (e.rel.clone(), e.clone()))
.collect();
let src_set: HashSet<&PathBuf> = src_entries.iter().map(|e| &e.rel).collect();
let mut stats = Stats::default();
for e in &src_entries {
control.checkpoint()?;
if check_relative(&e.rel).is_err() {
fail(
reporter,
&mut stats,
&e.rel,
"path escapes destination root",
);
continue;
}
let mtime = FileTime::from_unix_time(e.mtime_s, e.mtime_ns);
let existing_entry = existing_map.get(&e.rel);
match &e.kind {
NetKind::Dir => {
let full = root_canon.join(&e.rel);
if let Err(err) = std::fs::create_dir_all(&full) {
fail(reporter, &mut stats, &e.rel, &err.to_string());
continue;
}
apply_meta(&full, e.mode, mtime, options, false);
let action = bump(&mut stats, existing_entry.is_some());
reporter.event(Event::DirDone {
rel: e.rel.clone(),
action,
});
}
NetKind::Symlink(target) => {
let dst = match ensure_and_contain(&root_canon, &e.rel) {
Ok(p) => p,
Err(err) => {
fail(reporter, &mut stats, &e.rel, &err.to_string());
continue;
}
};
if let Ok(meta) = std::fs::symlink_metadata(&dst) {
if meta.is_dir() {
let _ = std::fs::remove_dir_all(&dst);
} else {
let _ = std::fs::remove_file(&dst);
}
}
#[cfg(unix)]
{
use std::path::Component;
if target.is_absolute()
|| target.components().any(|c| c == Component::ParentDir)
{
fail(
reporter,
&mut stats,
&e.rel,
"symlink target escapes destination root",
);
continue;
}
}
if let Err(err) = symlink_create(target, &dst) {
fail(reporter, &mut stats, &e.rel, &err.to_string());
continue;
}
if options.preserve_mtime {
let _ = set_symlink_mtime(&dst, mtime);
}
let action = bump(&mut stats, existing_entry.is_some());
reporter.event(Event::SymlinkDone {
rel: e.rel.clone(),
action,
});
}
NetKind::File => {
let is_existing_file = existing_entry.is_some_and(Entry::is_file);
let unchanged = !options.checksum
&& is_existing_file
&& existing_entry.is_some_and(|x| {
x.len == e.len
&& x.mtime.unix_seconds() == e.mtime_s
&& x.mtime.nanoseconds() == e.mtime_ns
});
if unchanged {
stats.skipped += 1;
reporter.event(Event::Skipped { rel: e.rel.clone() });
continue;
}
let dst = match ensure_and_contain(&root_canon, &e.rel) {
Ok(p) => p,
Err(err) => {
fail(reporter, &mut stats, &e.rel, &err.to_string());
continue;
}
};
let Some(new_bytes) = fetch_bytes(conn, e, &dst, is_existing_file, options)? else {
fail(
reporter,
&mut stats,
&e.rel,
"source unreadable or delta failed",
);
continue;
};
if let Err(err) = write_atomic(&dst, &new_bytes, e.mode, mtime, options) {
fail(reporter, &mut stats, &e.rel, &err.to_string());
continue;
}
stats.bytes += new_bytes.len() as u64;
let action = bump(&mut stats, is_existing_file);
reporter.event(Event::FileDone {
rel: e.rel.clone(),
action,
bytes: new_bytes.len() as u64,
});
}
}
}
if options.delete {
let mut victims: Vec<&Entry> = existing
.iter()
.filter(|x| !src_set.contains(&x.rel))
.collect();
victims.sort_by(|a, b| b.rel.cmp(&a.rel));
for victim in victims {
control.checkpoint()?;
let full = root_canon.join(&victim.rel);
let removed = if victim.is_dir() {
std::fs::remove_dir(&full).or_else(|_| std::fs::remove_dir_all(&full))
} else {
std::fs::remove_file(&full)
};
if removed.is_ok() {
stats.deleted += 1;
reporter.event(Event::Deleted {
rel: victim.rel.clone(),
});
}
}
}
send_msg(conn, &Msg::Finished(stats.clone()))?;
Ok(stats)
}
fn fetch_bytes<C: Read + Write>(
conn: &mut C,
e: &NetEntry,
dst: &Path,
is_existing_file: bool,
options: NetOptions,
) -> Result<Option<Vec<u8>>> {
let use_delta = !options.whole_file && is_existing_file;
if use_delta {
let old = std::fs::read(dst).unwrap_or_default();
let sig = Signature::compute(&old, None);
send_msg(conn, &Msg::Request(Request::Delta(e.rel.clone(), sig)))?;
match recv_msg(conn)? {
Msg::Data(Data::Delta(d)) => Ok(apply(&old, &d).map_err(|e| tracing::debug!("failed to apply delta: {e}")).ok()),
Msg::Data(Data::Whole { bytes, zstd }) => Ok(unpack(bytes, zstd)),
Msg::Data(Data::NotFound) => Ok(None),
Msg::Error(e) => Err(Error::Protocol(format!("peer error: {e}"))),
other => Err(Error::Protocol(format!("receiver: unexpected {other:?}"))),
}
} else {
send_msg(conn, &Msg::Request(Request::Whole(e.rel.clone())))?;
match recv_msg(conn)? {
Msg::Data(Data::Whole { bytes, zstd }) => Ok(unpack(bytes, zstd)),
Msg::Data(Data::NotFound) => Ok(None),
Msg::Error(e) => Err(Error::Protocol(format!("peer error: {e}"))),
other => Err(Error::Protocol(format!("receiver: unexpected {other:?}"))),
}
}
}
fn unpack(bytes: Vec<u8>, zstd: bool) -> Option<Vec<u8>> {
if zstd {
zstd::decode_all(bytes.as_slice()).map_err(|e| tracing::debug!("failed to decompress: {e}")).ok()
} else {
Some(bytes)
}
}
fn fail<R: Reporter>(reporter: &R, stats: &mut Stats, rel: &Path, error: &str) {
stats.errors += 1;
reporter.event(Event::Failed {
rel: rel.to_path_buf(),
error: error.to_string(),
});
}
fn bump(stats: &mut Stats, existed: bool) -> Action {
if existed {
stats.updated += 1;
Action::Update
} else {
stats.copied += 1;
Action::Copy
}
}
fn apply_meta(path: &Path, mode: u32, mtime: FileTime, options: NetOptions, _is_file: bool) {
if options.preserve_mode {
let _ = set_mode(path, mode);
}
if options.preserve_mtime {
let _ = set_mtime(path, mtime);
}
}
fn ensure_and_contain(root_canon: &Path, rel: &Path) -> Result<PathBuf> {
let target = root_canon.join(rel);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
}
contained_target(root_canon, &target)
}
fn write_atomic(
dst: &Path,
bytes: &[u8],
mode: u32,
mtime: FileTime,
options: NetOptions,
) -> Result<()> {
let parent = dst
.parent()
.ok_or_else(|| Error::Containment(dst.to_path_buf()))?;
let tmp = parent.join(format!(".ripsync.tmp.{:016x}", rand::random::<u64>()));
std::fs::write(&tmp, bytes).map_err(|e| Error::io(&tmp, e))?;
apply_meta(&tmp, mode, mtime, options, true);
if let Ok(meta) = std::fs::symlink_metadata(dst) {
if meta.is_dir() {
if let Err(e) = std::fs::remove_dir_all(dst) {
eprintln!("Warning: failed to remove directory {:?}: {}", dst, e);
}
}
}
if let Err(e) = atomic_replace(&tmp, dst) {
let _ = std::fs::remove_file(&tmp);
return Err(Error::io(dst, e));
}
Ok(())
}
#[cfg(not(windows))]
fn atomic_replace(tmp: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::rename(tmp, dst)
}
#[cfg(windows)]
fn atomic_replace(tmp: &Path, dst: &Path) -> std::io::Result<()> {
crate::io::windows::replace_file(tmp, dst)
}
#[cfg(unix)]
fn symlink_create(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
fn symlink_create(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::windows::fs::symlink_file(target, link)
}