use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use crate::protocol::framing::{self};
use crate::protocol::message::{self, ErrorCode, 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};
#[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 struct TreeReceiveRequest {
pub root: PathBuf,
pub force_checksum: bool,
pub no_delete: bool,
pub excludes: Vec<String>,
}
#[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,
})
.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,
});
}
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 send_tree_begin(
stream: &mut impl Write,
flags: u16,
excludes: &[String],
manifest: &TreeManifest,
) -> Result<(), SyncError> {
TreeBegin {
flags,
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(())
}
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 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
};
send_tree_begin(stream, flags, &request.excludes, &manifest)?;
let (peer_begin, peer_manifest) = read_tree_begin(stream)?;
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;
for (idx, rel) in actions.iter().enumerate() {
progress(TreeProgress::FileStart(idx, total));
let path = abs_path(&request.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: path.clone(),
display_path: rel.clone(),
mtime,
force_checksum: checksum,
tree: true,
};
let rel_for_progress = rel.clone();
let mut file_progress = |p: Progress| {
progress(TreeProgress::FileProgress {
rel_path: rel_for_progress.clone(),
progress: p,
});
};
let outcome = match sync::send(stream, &send_req, pipeline, &mut file_progress) {
Ok(o) => o,
Err(e) => {
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &e.to_string());
return Err(e);
}
};
match outcome {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
progress(TreeProgress::FileDone(idx, total));
}
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 flags = if request.force_checksum {
TREE_FLAG_CHECKSUM
} else {
0
} | if request.no_delete {
TREE_FLAG_NO_DELETE
} 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, &[], &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 total = conversation_paths.len();
let mut transferred_bytes = 0u64;
let mut in_sync = 0usize;
for (idx, rel) in conversation_paths.iter().enumerate() {
progress(TreeProgress::FileStart(idx, total));
let path = abs_path(&request.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, };
let rel_for_progress = rel.clone();
let mut file_progress = |p: Progress| {
progress(TreeProgress::FileProgress {
rel_path: rel_for_progress.clone(),
progress: p,
});
};
let outcome = match sync::receive(stream, &recv_req, pipeline, &mut file_progress) {
Ok(o) => o,
Err(e) => {
let _ = send_error_and_finish(stream, ErrorCode::Aborted, &e.to_string());
return Err(e);
}
};
match outcome {
SyncOutcome::InSync => in_sync += 1,
SyncOutcome::Transferred { bytes, .. } => transferred_bytes += bytes,
}
progress(TreeProgress::FileDone(idx, total));
}
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,
})
}