use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use crate::protocol::framing::{self};
use crate::protocol::message::{self, ErrorCode, MessageError, TreeBegin, TreeEnd, TreeEntryWire};
use crate::protocol::sync::{
self, Progress, ReceiveRequest, SendRequest, SyncError, SyncOutcome, send_error_and_finish,
};
use crate::tree::{self, EntryKind, TREE_FLAG_CHECKSUM, TREE_FLAG_NO_DELETE, TreeManifest, Triage};
pub const MAX_PARALLEL: u16 = 16;
enum ConvEvent {
Start(#[allow(dead_code)] String),
Progress(String, Progress),
Done(#[allow(dead_code)] String, SyncOutcome),
}
fn pipeline_clone(p: &crate::chunking::PipelineConfig) -> crate::chunking::PipelineConfig {
crate::chunking::PipelineConfig {
read_buffer_size: p.read_buffer_size,
chunk_queue_size: p.chunk_queue_size,
hash_workers: p.hash_workers,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TreeProgress {
Scanning,
FileStart(usize, usize),
FileProgress {
rel_path: String,
progress: Progress,
},
FileDone(usize, usize),
Deleting(usize, u64),
}
pub struct TreeSendRequest {
pub root: PathBuf,
pub force_checksum: bool,
pub no_delete: bool,
pub excludes: Vec<String>,
pub parallel: u16,
pub connect_worker: std::sync::Arc<dyn Fn(u16) -> io::Result<Box<dyn ReadWrite>> + Send + Sync>,
}
pub struct TreeReceiveRequest {
pub root: PathBuf,
pub force_checksum: bool,
pub no_delete: bool,
pub excludes: Vec<String>,
pub accept_worker: std::sync::Arc<dyn Fn() -> io::Result<Box<dyn ReadWrite>> + Send + Sync>,
}
pub trait ReadWrite: Read + Write + Send {}
impl<T: Read + Write + Send> ReadWrite for T {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeOutcome {
pub files: usize,
pub transferred_bytes: u64,
pub in_sync: usize,
pub deleted: u32,
pub deleted_bytes: u64,
}
fn manifest_to_wire(m: &TreeManifest) -> Vec<TreeEntryWire> {
m.entries()
.map(|e| TreeEntryWire {
rel_path: e.rel_path.clone(),
kind: e.kind as u8,
size: e.size,
mtime_secs: e.mtime_secs,
mtime_nanos: e.mtime_nanos,
mode: e.mode,
})
.collect()
}
pub fn manifest_from_wire(wire: &[TreeEntryWire]) -> Result<TreeManifest, SyncError> {
let mut m = TreeManifest::new();
for e in wire {
tree::validate_rel_path(&e.rel_path)
.map_err(|why| SyncError::Protocol(format!("bad tree path: {why}")))?;
let kind = EntryKind::from_u8(e.kind)
.ok_or_else(|| SyncError::Protocol(format!("bad tree entry kind {}", e.kind)))?;
if kind == EntryKind::Dir && e.size != 0 {
return Err(SyncError::Protocol(format!(
"tree dir entry {} carries a size",
e.rel_path
)));
}
m.insert(crate::tree::TreeEntry {
rel_path: e.rel_path.clone(),
kind,
size: e.size,
mtime_secs: e.mtime_secs,
mtime_nanos: e.mtime_nanos,
mode: e.mode,
});
}
Ok(m)
}
fn read_tree_begin(stream: &mut impl Read) -> Result<(TreeBegin, TreeManifest), SyncError> {
let frame = framing::read_frame(stream)?;
let begin = TreeBegin::from_frame(frame)?;
let manifest = manifest_from_wire(&begin.entries)?;
Ok((begin, manifest))
}
fn sender_mode_of(peer_manifest: &TreeManifest, rel: &str, metadata: bool) -> Option<u32> {
if !metadata {
return None;
}
peer_manifest.get(rel).map(|e| e.mode).filter(|m| *m != 0)
}
fn sender_modes_of(
peer_manifest: &TreeManifest,
metadata: bool,
) -> std::collections::HashMap<String, u32> {
if !metadata {
return std::collections::HashMap::new();
}
peer_manifest
.entries()
.map(|e| (e.rel_path.clone(), e.mode))
.filter(|(_, m)| *m != 0)
.collect()
}
fn send_tree_begin(
stream: &mut impl Write,
flags: u16,
parallel: u16,
excludes: &[String],
manifest: &TreeManifest,
) -> Result<(), SyncError> {
TreeBegin {
flags,
parallel,
excludes: excludes.to_vec(),
entries: manifest_to_wire(manifest),
}
.into_frame()?
.encode(stream)?;
stream.flush()?;
Ok(())
}
fn abs_path(root: &Path, rel: &str) -> PathBuf {
root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR))
}
fn ensure_parent_dir(target: &Path) -> io::Result<()> {
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(())
}
fn run_sender_conversation<S>(
stream: &mut S,
root: &Path,
rel: &str,
checksum: bool,
pipeline: &crate::chunking::PipelineConfig,
progress: &mut dyn FnMut(Progress),
) -> Result<SyncOutcome, SyncError>
where
S: Read + Write,
{
let path = abs_path(root, rel);
let meta = std::fs::metadata(&path).map_err(|e| {
let err = SyncError::Protocol(format!("sender file vanished: {rel}: {e}"));
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &err.to_string());
err
})?;
let mtime = meta.modified().ok();
let send_req = SendRequest {
path,
display_path: rel.to_string(),
mtime,
force_checksum: checksum,
tree: true,
};
let outcome = match sync::send(stream, &send_req, pipeline, progress) {
Ok(o) => o,
Err(e) => {
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &e.to_string());
return Err(e);
}
};
Ok(outcome)
}
pub fn tree_send<S>(
stream: &mut S,
request: &TreeSendRequest,
pipeline: &crate::chunking::PipelineConfig,
progress: &mut dyn FnMut(TreeProgress),
) -> Result<TreeOutcome, SyncError>
where
S: Read + Write,
{
progress(TreeProgress::Scanning);
let excludes = tree::ExcludeSet::from_patterns(&request.excludes)
.map_err(|e| SyncError::Protocol(format!("bad exclude pattern: {e}")))?;
let manifest = TreeManifest::scan_with_excludes(&request.root, &excludes).map_err(|e| {
SyncError::Protocol(format!(
"cannot scan tree {}: {}",
request.root.display(),
e
))
})?;
let metadata = cfg!(unix);
let negation = excludes.has_negations();
let parallel = request.parallel;
let flags = if request.force_checksum {
TREE_FLAG_CHECKSUM
} else {
0
} | if request.no_delete {
TREE_FLAG_NO_DELETE
} else {
0
} | if !request.excludes.is_empty() {
message::TREE_FLAG_EXCLUDES
} else {
0
} | if parallel > 1 {
message::TREE_FLAG_PARALLEL
} else {
0
} | if metadata {
message::TREE_FLAG_METADATA
} else {
0
} | if negation {
message::TREE_FLAG_NEGATION
} else {
0
};
send_tree_begin(stream, flags, parallel, &request.excludes, &manifest)?;
let (peer_begin, peer_manifest) = read_tree_begin(stream)?;
if peer_begin.parallel != parallel {
return Err(SyncError::Protocol(format!(
"peer replied with parallel={} (we announced {parallel})",
peer_begin.parallel
)));
}
let checksum = request.force_checksum || (peer_begin.flags & TREE_FLAG_CHECKSUM) != 0;
let _ = request.no_delete || (peer_begin.flags & TREE_FLAG_NO_DELETE) != 0;
let mut actions: Vec<String> = Vec::new();
for entry in manifest.entries() {
let action = tree::triage(
&entry.rel_path,
Some(entry),
peer_manifest.get(&entry.rel_path),
checksum,
);
match action {
Triage::Sync | Triage::Transfer => actions.push(entry.rel_path.clone()),
Triage::Conflict {
sender_kind: EntryKind::File,
..
} => actions.push(entry.rel_path.clone()),
_ => {}
}
}
actions.sort();
let total = actions.len();
let mut transferred_bytes = 0u64;
let mut in_sync = 0usize;
if request.parallel <= 1 {
for (idx, rel) in actions.iter().enumerate() {
progress(TreeProgress::FileStart(idx, total));
let outcome = run_sender_conversation(
stream,
&request.root,
rel,
checksum,
pipeline,
&mut |p| {
progress(TreeProgress::FileProgress {
rel_path: rel.clone(),
progress: p,
})
},
)?;
match outcome {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
progress(TreeProgress::FileDone(idx, total));
}
} else {
let k = request.parallel;
if !(2..=MAX_PARALLEL).contains(&k) {
return Err(SyncError::Protocol(format!(
"parallel={k} outside 2..={MAX_PARALLEL} (§16.8)"
)));
}
let mut partitions: Vec<Vec<String>> = vec![Vec::new(); k as usize];
for (i, rel) in actions.iter().enumerate() {
partitions[i % k as usize].push(rel.clone());
}
let (progress_tx, progress_rx) = std::sync::mpsc::channel::<(String, Progress)>();
let mut handles = Vec::new();
let mut worker_errors: Vec<Option<SyncError>> = (0..k as usize).map(|_| None).collect();
for w in 1..k {
let connect = std::sync::Arc::clone(&request.connect_worker);
let root = request.root.clone();
let part = partitions[w as usize].clone();
let pipeline = pipeline_clone(pipeline);
let tx = progress_tx.clone();
let connect_index = w;
handles.push(std::thread::spawn(move || {
let mut worker_stream: Box<dyn ReadWrite> = match connect(connect_index) {
Ok(s) => s,
Err(e) => return (Vec::new(), Some(SyncError::Io(e))),
};
if let Err(e) = (|| -> Result<(), SyncError> {
message::TreeWorker {
worker_index: connect_index,
}
.into_frame()?
.encode(&mut worker_stream)?;
worker_stream.flush()?;
let frame = framing::read_frame(&mut worker_stream)?;
if frame.ftype != framing::T_TREE_WORKER {
return Err(SyncError::Protocol(format!(
"expected TREE_WORKER ack, got 0x{:02X}",
frame.ftype
)));
}
Ok(())
})() {
return (Vec::new(), Some(e));
}
let mut outcomes = Vec::new();
for rel in &part {
let _ = message::TreeFile {
rel_path: rel.clone(),
}
.into_frame()
.and_then(|f| f.encode(&mut worker_stream));
let _ = worker_stream.flush();
let rel_c = rel.clone();
match run_sender_conversation(
&mut worker_stream,
&root,
rel,
checksum,
&pipeline,
&mut |p| {
let _ = tx.send((rel_c.clone(), p));
},
) {
Ok(o) => outcomes.push(o),
Err(e) => {
let _ = send_error_and_finish(
&mut worker_stream,
ErrorCode::Aborted,
&e.to_string(),
);
return (outcomes, Some(e)); }
}
}
(outcomes, None)
}));
}
let mut control_error: Option<SyncError> = None;
for (idx, rel) in partitions[0].iter().enumerate() {
progress(TreeProgress::FileStart(idx, total));
let _ = message::TreeFile {
rel_path: rel.clone(),
}
.into_frame()
.and_then(|f| f.encode(stream));
stream.flush()?;
let outcome = match run_sender_conversation(
stream,
&request.root,
rel,
checksum,
pipeline,
&mut |p| {
progress(TreeProgress::FileProgress {
rel_path: rel.clone(),
progress: p,
})
},
) {
Ok(o) => o,
Err(e) => {
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &e.to_string());
control_error = Some(e);
break;
}
};
match outcome {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
progress(TreeProgress::FileDone(idx, total));
}
for (w, handle) in handles.into_iter().enumerate() {
let (outcomes, err) = handle
.join()
.map_err(|_| SyncError::Protocol("worker thread panicked".to_string()))?;
let w = w + 1; for o in outcomes {
match o {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
}
worker_errors[w] = err;
}
drop(progress_tx);
while let Ok((rel_path, p)) = progress_rx.try_recv() {
progress(TreeProgress::FileProgress {
rel_path,
progress: p,
});
}
if let Some(e) = worker_errors.into_iter().flatten().next() {
return Err(e);
}
if let Some(e) = control_error {
return Err(e);
}
}
message::TreeEnd::default().into_frame()?.encode(stream)?;
stream.flush()?;
let frame = framing::read_frame(stream)?;
match frame.ftype {
framing::T_TREE_END => {
let report = TreeEnd::from_frame(frame)?;
Ok(TreeOutcome {
files: total,
transferred_bytes,
in_sync,
deleted: report.deleted_count,
deleted_bytes: report.deleted_bytes,
})
}
framing::T_ERROR => {
let (code, msg) = message::read_error_payload(&frame)?;
Err(SyncError::Peer(code, msg))
}
other => Err(SyncError::Protocol(format!(
"expected TREE_END, got 0x{:02X}",
other
))),
}
}
pub fn tree_receive<S>(
stream: &mut S,
request: &TreeReceiveRequest,
pipeline: &crate::chunking::PipelineConfig,
progress: &mut dyn FnMut(TreeProgress),
) -> Result<TreeOutcome, SyncError>
where
S: Read + Write,
{
progress(TreeProgress::Scanning);
let (peer_begin, peer_manifest) = read_tree_begin(stream)?;
let sender_excludes = tree::ExcludeSet::from_patterns(&peer_begin.excludes)
.map_err(|e| SyncError::Protocol(format!("peer sent bad exclude pattern: {e}")))?;
let manifest =
TreeManifest::scan_with_excludes(&request.root, &sender_excludes).map_err(|e| {
SyncError::Protocol(format!(
"cannot scan tree {}: {}",
request.root.display(),
e
))
})?;
let parallel = peer_begin.parallel;
let metadata = (peer_begin.flags & message::TREE_FLAG_METADATA) != 0;
let negation = (peer_begin.flags & message::TREE_FLAG_NEGATION) != 0;
let flags = if request.force_checksum {
TREE_FLAG_CHECKSUM
} else {
0
} | if request.no_delete {
TREE_FLAG_NO_DELETE
} else {
0
} | if parallel > 1 {
message::TREE_FLAG_PARALLEL
} else {
0
} | if metadata {
message::TREE_FLAG_METADATA
} else {
0
} | if negation {
message::TREE_FLAG_NEGATION
} else {
0
};
let own_excludes = tree::ExcludeSet::from_patterns(&request.excludes)
.map_err(|e| SyncError::Protocol(format!("bad exclude pattern: {e}")))?;
send_tree_begin(stream, flags, parallel, &[], &manifest)?;
let checksum = request.force_checksum || (peer_begin.flags & TREE_FLAG_CHECKSUM) != 0;
let no_delete = request.no_delete || (peer_begin.flags & TREE_FLAG_NO_DELETE) != 0;
let mut conversation_paths: Vec<String> = Vec::new();
let mut deletions: Vec<(String, u64)> = Vec::new();
let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for e in manifest.entries() {
all_paths.insert(e.rel_path.clone());
}
for e in peer_manifest.entries() {
all_paths.insert(e.rel_path.clone());
}
for rel in &all_paths {
let action = tree::triage(rel, peer_manifest.get(rel), manifest.get(rel), checksum);
macro_rules! announce {
($err:expr) => {{
let err = $err;
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &err.to_string());
return Err(err);
}};
}
match action {
Triage::Skip => {}
Triage::Sync | Triage::Transfer => conversation_paths.push(rel.clone()),
Triage::CreateDir => {
let p = abs_path(&request.root, rel);
if let Err(e) = std::fs::create_dir_all(&p) {
announce!(SyncError::Protocol(format!("cannot create dir {rel}: {e}")));
}
}
Triage::Delete => {
if let Some(entry) = manifest.get(rel) {
if entry.kind == EntryKind::File {
deletions.push((rel.clone(), entry.size));
} else {
deletions.push((rel.clone(), 0));
}
}
}
Triage::Conflict { sender_kind, .. } => {
match sender_kind {
EntryKind::File => {
let p = abs_path(&request.root, rel);
if p.is_dir()
&& let Err(e) = std::fs::remove_dir_all(&p)
{
announce!(SyncError::Protocol(format!(
"cannot replace dir {rel} with file: {e}"
)));
}
conversation_paths.push(rel.clone());
}
EntryKind::Dir => {
let p = abs_path(&request.root, rel);
if p.is_file()
&& let Err(e) = std::fs::remove_file(&p)
{
announce!(SyncError::Protocol(format!(
"cannot replace file {rel} with dir: {e}"
)));
}
if let Err(e) = std::fs::create_dir_all(&p) {
announce!(SyncError::Protocol(format!("cannot create dir {rel}: {e}")));
}
}
}
}
}
}
let mut sender_end_consumed = false;
let k = parallel;
let total = conversation_paths.len();
let mut transferred_bytes = 0u64;
let mut in_sync = 0usize;
let mut remaining: std::collections::HashSet<String> =
conversation_paths.iter().cloned().collect();
struct ConvCtx<'a> {
root: &'a Path,
checksum: bool,
pipeline: &'a crate::chunking::PipelineConfig,
}
fn run_one<S: Read + Write>(
ctx: &ConvCtx<'_>,
stream: &mut S,
rel: &str,
idx: usize,
total: usize,
sender_mode: Option<u32>,
progress: &mut dyn FnMut(TreeProgress),
) -> Result<(SyncOutcome, String), SyncError> {
let ConvCtx {
root,
checksum,
pipeline,
} = *ctx;
progress(TreeProgress::FileStart(idx, total));
let path = abs_path(root, rel);
if let Err(e) = ensure_parent_dir(&path) {
let err = SyncError::Protocol(format!("cannot create parent dirs for {rel}: {e}"));
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &err.to_string());
return Err(err);
}
let recv_req = ReceiveRequest {
path: path.clone(),
local_source: path.clone(),
force_checksum: checksum,
tree: true,
apply_mtime: None, apply_mode: sender_mode,
};
let rel_c = rel.to_string();
let outcome = match sync::receive(stream, &recv_req, pipeline, &mut |p| {
progress(TreeProgress::FileProgress {
rel_path: rel_c.clone(),
progress: p,
});
}) {
Ok(o) => o,
Err(e) => {
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &e.to_string());
return Err(e);
}
};
progress(TreeProgress::FileDone(idx, total));
Ok((outcome, rel.to_string()))
}
if k <= 1 {
let ctx = ConvCtx {
root: &request.root,
checksum,
pipeline,
};
for (idx, rel) in conversation_paths.iter().enumerate() {
let sender_mode = sender_mode_of(&peer_manifest, rel, metadata);
let (outcome, _) = run_one(&ctx, stream, rel, idx, total, sender_mode, progress)?;
match outcome {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
remaining.remove(rel);
}
} else {
let mut worker_streams: Vec<Option<Box<dyn ReadWrite>>> =
(0..k as usize).map(|_| None).collect();
for _ in 1..k {
let (mut s, announced) = {
let s = (request.accept_worker)().map_err(|e| {
let err = SyncError::Io(e);
let _ = send_error_and_finish(
stream,
ErrorCode::Aborted,
&format!("cannot accept worker: {err}"),
);
err
})?;
let mut s = s;
let frame = framing::read_frame(&mut *s)?;
let announced = message::TreeWorker::from_frame(frame).map_err(|e| match e {
MessageError::PeerError(code, msg) => SyncError::Peer(code, msg),
other => SyncError::Protocol(format!("bad TREE_WORKER: {other}")),
})?;
(s, announced)
};
let w = announced.worker_index;
if w == 0 || w >= k {
let err =
SyncError::Protocol(format!("worker announced index {w} (valid: 1..{k})"));
let _ = send_error_and_finish(&mut *s, ErrorCode::Aborted, &err.to_string());
return Err(err);
}
if worker_streams[w as usize].is_some() {
let err =
SyncError::Protocol(format!("duplicate worker announcement for index {w}"));
let _ = send_error_and_finish(&mut *s, ErrorCode::Aborted, &err.to_string());
return Err(err);
}
message::TreeWorker { worker_index: w }
.into_frame()?
.encode(&mut *s)?;
s.flush()?;
worker_streams[w as usize] = Some(s);
}
let remaining = std::sync::Arc::new(std::sync::Mutex::new(remaining));
let first_error = std::sync::Arc::new(std::sync::Mutex::new(None::<SyncError>));
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let (event_tx, event_rx) = std::sync::mpsc::channel::<ConvEvent>();
struct Shared {
remaining: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
first_error: std::sync::Arc<std::sync::Mutex<Option<SyncError>>>,
root: PathBuf,
checksum: bool,
sender_modes: std::collections::HashMap<String, u32>,
pipeline: crate::chunking::PipelineConfig,
}
impl Shared {
fn record_error(&self, err: SyncError) {
let mut first = self.first_error.lock().unwrap();
if first.is_none() {
*first = Some(err);
}
self.cancel
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
let shared = std::sync::Arc::new(Shared {
remaining: std::sync::Arc::clone(&remaining),
cancel: std::sync::Arc::clone(&cancel),
first_error: std::sync::Arc::clone(&first_error),
root: request.root.clone(),
checksum,
sender_modes: sender_modes_of(&peer_manifest, metadata),
pipeline: pipeline_clone(pipeline),
});
let serve_connection = {
let shared = std::sync::Arc::clone(&shared);
move |mut s: Box<dyn ReadWrite>, event_tx: std::sync::mpsc::Sender<ConvEvent>| {
loop {
if shared.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return; }
let frame = match framing::read_frame(&mut *s) {
Ok(f) => f,
Err(framing::FrameError::Eof) => return,
Err(e) => {
shared.record_error(SyncError::from(e));
return;
}
};
let named = match message::TreeFile::from_frame(frame) {
Ok(n) => n,
Err(MessageError::PeerError(code, msg)) => {
shared.record_error(SyncError::Peer(code, msg));
return;
}
Err(other) => {
shared.record_error(SyncError::Protocol(format!(
"bad TREE_FILE: {other}"
)));
return;
}
};
let taken = shared.remaining.lock().unwrap().remove(&named.rel_path);
if !taken {
let err = SyncError::Protocol(format!(
"unexpected conversation: {} (not in the expected set, or already done)",
named.rel_path
));
let _ =
send_error_and_finish(&mut *s, ErrorCode::Aborted, &err.to_string());
shared.record_error(err);
return;
}
let rel = named.rel_path.clone();
let _ = event_tx.send(ConvEvent::Start(rel.clone()));
let sender_mode = shared.sender_modes.get(&rel).copied();
let outcome = run_one(
&ConvCtx {
root: &shared.root,
checksum: shared.checksum,
pipeline: &shared.pipeline,
},
&mut s,
&rel,
0,
0,
sender_mode,
&mut |p| {
if let TreeProgress::FileProgress { rel_path, progress } = p {
let _ = event_tx.send(ConvEvent::Progress(rel_path, progress));
}
},
);
match outcome {
Ok((o, _)) => {
let _ = event_tx.send(ConvEvent::Done(rel, o));
}
Err(e) => {
let _ =
send_error_and_finish(&mut *s, ErrorCode::Aborted, &e.to_string());
shared.record_error(e);
return;
}
}
}
}
};
let mut handles = Vec::new();
for slot in worker_streams.into_iter().flatten() {
let serve = serve_connection.clone();
let tx = event_tx.clone();
handles.push(std::thread::spawn(move || {
serve(slot, tx);
}));
}
drop(event_tx);
let mut idx = 0usize;
let mut session_error: Option<SyncError> = None;
let mut saw_tree_end = false;
loop {
while let Ok(ev) = event_rx.try_recv() {
match ev {
ConvEvent::Start(_) => {
progress(TreeProgress::FileStart(idx, total));
}
ConvEvent::Progress(rel_path, p) => {
progress(TreeProgress::FileProgress {
rel_path,
progress: p,
});
}
ConvEvent::Done(_, o) => {
progress(TreeProgress::FileDone(idx, total));
idx += 1;
match o {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
}
}
}
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
break; }
let frame = match framing::read_frame(stream) {
Ok(f) => f,
Err(framing::FrameError::Eof) => {
break;
}
Err(e) => {
session_error = Some(SyncError::from(e));
break;
}
};
match frame.ftype {
framing::T_TREE_FILE => {
let named = match message::TreeFile::from_frame(frame) {
Ok(n) => n,
Err(MessageError::PeerError(code, msg)) => {
session_error = Some(SyncError::Peer(code, msg));
break;
}
Err(other) => {
session_error =
Some(SyncError::Protocol(format!("bad TREE_FILE: {other}")));
break;
}
};
let taken = remaining.lock().unwrap().remove(&named.rel_path);
if !taken {
session_error = Some(SyncError::Protocol(format!(
"unexpected conversation: {} (not in the expected set, or already done)",
named.rel_path
)));
break;
}
let rel = named.rel_path.clone();
let sender_mode = peer_manifest.get(&rel).filter(|_| metadata).map(|e| e.mode);
let outcome = run_one(
&ConvCtx {
root: &request.root,
checksum,
pipeline,
},
stream,
&rel,
idx,
total,
sender_mode,
&mut |tp| progress(tp),
);
idx += 1;
match outcome {
Ok((SyncOutcome::InSync, _)) => in_sync += 1,
Ok((SyncOutcome::Transferred { bytes, .. }, _)) => {
transferred_bytes += bytes
}
Err(e) => {
let _ =
send_error_and_finish(stream, ErrorCode::Aborted, &e.to_string());
session_error = Some(e);
break;
}
}
}
framing::T_TREE_END => {
saw_tree_end = true;
break;
}
framing::T_ERROR => {
session_error = Some(match message::read_error_payload(&frame) {
Ok((code, msg)) => SyncError::Peer(code, msg),
Err(e) => SyncError::Protocol(format!("bad ERROR frame: {e}")),
});
break;
}
other => {
session_error = Some(SyncError::Protocol(format!(
"expected TREE_FILE or TREE_END on the control connection, got 0x{:02X}",
other
)));
break;
}
}
}
for h in handles {
if h.join().is_err() && session_error.is_none() {
session_error = Some(SyncError::Protocol("worker thread panicked".into()));
}
}
while let Ok(ev) = event_rx.try_recv() {
match ev {
ConvEvent::Start(_) => {
progress(TreeProgress::FileStart(idx, total));
}
ConvEvent::Progress(rel_path, p) => {
progress(TreeProgress::FileProgress {
rel_path,
progress: p,
});
}
ConvEvent::Done(_, o) => {
progress(TreeProgress::FileDone(idx, total));
idx += 1;
match o {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
}
}
}
let recorded = first_error.lock().unwrap().take();
let has_error = session_error.is_some() || recorded.is_some();
if !has_error {
if !saw_tree_end {
return Err(SyncError::Protocol(
"sender ended the control connection without TREE_END".to_string(),
));
}
let pending = remaining.lock().unwrap().len();
if pending > 0 {
return Err(SyncError::Protocol(format!(
"sender's TREE_END arrived with {pending} expected conversations never announced"
)));
}
sender_end_consumed = true;
} else {
let err = session_error
.or(recorded)
.unwrap_or_else(|| SyncError::Protocol("session failed".to_string()));
return Err(err);
}
}
if !sender_end_consumed {
let sender_end_frame = framing::read_frame(stream)?;
match sender_end_frame.ftype {
framing::T_TREE_END => {
let end = TreeEnd::from_frame(sender_end_frame)?;
if end.deleted_count != 0 || end.deleted_bytes != 0 {
return Err(SyncError::Protocol(
"sender TREE_END must be empty (delete report is receiver→sender)"
.to_string(),
));
}
}
framing::T_ERROR => {
let (code, msg) = message::read_error_payload(&sender_end_frame)?;
return Err(SyncError::Peer(code, msg));
}
other => {
return Err(SyncError::Protocol(format!(
"expected TREE_END barrier, got 0x{:02X}",
other
)));
}
}
}
let mut deleted_count: u32 = 0;
let mut deleted_bytes: u64 = 0;
if !no_delete && !deletions.is_empty() {
deletions.sort_by(|a, b| b.0.cmp(&a.0));
for (i, (rel, size)) in deletions.iter().enumerate() {
let kind = manifest.get(rel).map(|e| e.kind).unwrap_or(EntryKind::File);
if own_excludes.matches_path_or_ancestor(rel, kind) {
continue;
}
let p = abs_path(&request.root, rel);
let res = if p.is_dir() {
std::fs::remove_dir(&p)
} else {
std::fs::remove_file(&p)
};
match res {
Ok(()) => {
deleted_count += 1;
deleted_bytes += size;
if p.is_file() {
crate::index::remove_index(&p);
}
}
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::NotADirectory => {}
Err(e) => {
return Err(SyncError::Protocol(format!("cannot delete {rel}: {e}")));
}
}
progress(TreeProgress::Deleting(i + 1, deleted_bytes));
}
}
TreeEnd {
deleted_count,
deleted_bytes,
}
.into_frame()?
.encode(stream)?;
stream.flush()?;
Ok(TreeOutcome {
files: total,
transferred_bytes,
in_sync,
deleted: deleted_count,
deleted_bytes,
})
}