use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use blake3::Hash;
use crate::chunking::{
Blake3Hasher, CHUNK_AVG, CHUNK_MAX, CHUNK_MIN, Chunk, ChunkEvent, FastCDCChunker,
PipelineConfig, chunk_root, process_file,
};
use crate::index::{FileIndex, load_index};
use crate::protocol::framing::{self, FrameError};
use crate::protocol::message::{
self, ErrorCode, FLAG_RESUMING, FLAG_TRUST_TIMES, Hello, ManifestChunk, Message, MessageError,
PlanEntry, Role,
};
use crate::reconstruction::{ChunkSource, FileExt, ReconstructionTask, Reconstructor};
thread_local! {
static DECOMPRESSOR: std::cell::RefCell<zstd::bulk::Decompressor<'static>> =
std::cell::RefCell::new(
zstd::bulk::Decompressor::new()
.expect("zstd decompressor context allocation failed"),
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LocalOrigin {
Temp { offset: u64, length: usize },
OldFile { offset: u64, length: usize },
}
#[derive(Debug)]
pub enum SyncError {
Peer(ErrorCode, String),
Io(io::Error),
Protocol(String),
Eof,
VerificationFailed,
}
impl std::fmt::Display for SyncError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SyncError::Peer(code, msg) => write!(f, "peer error {:?}: {}", code, msg),
SyncError::Io(e) => write!(f, "io error: {}", e),
SyncError::Protocol(msg) => write!(f, "protocol violation: {}", msg),
SyncError::Eof => write!(f, "connection closed unexpectedly"),
SyncError::VerificationFailed => write!(f, "verification failed"),
}
}
}
impl From<io::Error> for SyncError {
fn from(e: io::Error) -> Self {
SyncError::Io(e)
}
}
impl From<FrameError> for SyncError {
fn from(e: FrameError) -> Self {
match e {
FrameError::Eof => SyncError::Eof,
FrameError::Truncated => SyncError::Protocol("truncated frame".into()),
FrameError::Io(e) => SyncError::Io(e),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Progress {
Scanning(u64, u64),
WarmStart(usize),
Diffing,
Transferring(u64, u64),
Receiving(u64, u64),
Verifying,
NothingToDo,
}
pub struct SendRequest {
pub path: PathBuf,
pub display_path: String,
pub mtime: Option<SystemTime>,
pub force_checksum: bool,
pub tree: bool,
}
#[derive(Clone)]
pub struct ReceiveRequest {
pub path: PathBuf,
pub local_source: PathBuf,
pub force_checksum: bool,
pub tree: bool,
pub apply_mtime: Option<(u64, u32)>,
}
#[derive(Debug)]
pub enum SyncOutcome {
InSync,
Transferred { bytes: u64, chunks: usize },
}
fn unix_secs(t: Option<SystemTime>) -> u64 {
t.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub(crate) fn send_error_and_finish(
out: &mut (impl Write + ?Sized),
code: ErrorCode,
msg: &str,
) -> SyncError {
let frame = message::encode_error(code, msg);
let _ = frame.and_then(|f| f.encode(out));
let _ = out.flush();
SyncError::Peer(code, msg.to_string())
}
fn drain_for_close(stream: &mut impl Read) -> io::Result<()> {
let mut sink = [0u8; 8192];
let mut total = 0usize;
loop {
match stream.read(&mut sink) {
Ok(0) => return Ok(()), Ok(n) => {
total += n;
if total > 64 * 1024 * 1024 {
return Ok(());
}
}
Err(e) if e.kind() == io::ErrorKind::ConnectionReset => return Ok(()),
Err(e) => return Err(e),
}
}
}
fn validate_hello(peer: &Hello, ours_role: Role) -> Result<Hello, SyncError> {
if peer.version != super::VERSION {
return Err(SyncError::Peer(
ErrorCode::Version,
format!(
"unsupported protocol version {} (this peer speaks {})",
peer.version,
super::VERSION
),
));
}
if peer.role == ours_role {
return Err(SyncError::Protocol(format!(
"role conflict: both peers are {:?}",
ours_role
)));
}
Ok(peer.clone())
}
impl From<MessageError> for SyncError {
fn from(e: MessageError) -> Self {
match e {
MessageError::Frame(FrameError::Eof) => SyncError::Eof,
MessageError::Frame(FrameError::Truncated) => {
SyncError::Protocol("truncated frame".into())
}
MessageError::Frame(FrameError::Io(e)) => SyncError::Io(e),
MessageError::PeerError(code, msg) => SyncError::Peer(code, msg),
MessageError::UnexpectedType(t) => {
SyncError::Protocol(format!("unexpected frame type 0x{:02X}", t))
}
MessageError::Malformed(why) => {
SyncError::Protocol(format!("malformed frame: {}", why))
}
}
}
}
pub fn send<S>(
stream: &mut S,
request: &SendRequest,
pipeline: &PipelineConfig,
progress: &mut dyn FnMut(Progress),
) -> Result<SyncOutcome, SyncError>
where
S: Read + Write,
{
let file = File::open(&request.path)?;
let file_size = file.metadata()?.len();
let mtime = request
.mtime
.or_else(|| file.metadata().ok().and_then(|m| m.modified().ok()));
drop(file);
let (mtime_secs, mtime_nanos) = mtime
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| (d.as_secs(), d.subsec_nanos()))
.unwrap_or((0, 0));
progress(Progress::Scanning(0, file_size));
let warm = if request.force_checksum {
None
} else {
load_index(&request.path)
};
let warmed = warm.is_some();
let (source_chunks, root) = match warm {
Some(idx) => {
progress(Progress::WarmStart(idx.chunks.len()));
(idx.chunks, idx.root)
}
None => {
let (_scan_stats, chunks) = scan_with_progress(&request.path, pipeline, |hashed| {
progress(Progress::Scanning(hashed, file_size))
})?;
let root = chunk_root(&chunks);
(chunks, root)
}
};
progress(Progress::Diffing);
let hello_flags = if request.force_checksum {
0
} else {
FLAG_TRUST_TIMES
};
let hello = Hello {
version: super::VERSION,
flags: hello_flags,
role: Role::Sender,
cdc_min: CHUNK_MIN as u32,
cdc_avg: CHUNK_AVG as u32,
cdc_max: CHUNK_MAX as u32,
file_size,
file_mtime: mtime_secs,
file_mtime_nanos: mtime_nanos,
path: request.display_path.clone(),
};
let trust_own = (hello.flags & FLAG_TRUST_TIMES) != 0;
let hello_size = hello.file_size;
let hello_mtime = hello.file_mtime;
let hello_mtime_nanos = hello.file_mtime_nanos;
hello.into_frame()?.encode(stream)?;
stream.flush()?;
let peer = message::read_typed::<Hello>(stream)?;
let peer = match validate_hello(&peer, Role::Sender) {
Ok(h) => h,
Err(SyncError::Protocol(msg)) => {
return Err(send_error_and_finish(stream, ErrorCode::RoleConflict, &msg));
}
Err(SyncError::Peer(code, msg)) => {
return Err(send_error_and_finish(stream, code, &msg));
}
Err(other) => return Err(other),
};
let trust = trust_own && (peer.flags & FLAG_TRUST_TIMES) != 0;
if trust
&& hello_size == peer.file_size
&& hello_mtime != 0
&& hello_mtime == peer.file_mtime
&& hello_mtime_nanos == peer.file_mtime_nanos
{
message::encode_empty(framing::T_NOT_MODIFIED)?.encode(stream)?;
stream.flush()?;
if request.tree {
progress(Progress::NothingToDo);
return Ok(SyncOutcome::InSync);
}
let _ = drain_for_close(stream);
progress(Progress::NothingToDo);
return Ok(SyncOutcome::InSync);
}
if request.tree {
message::encode_empty(framing::T_FILE_SYNC)?.encode(stream)?;
stream.flush()?;
}
let mut manifest: HashMap<Hash, u64> = HashMap::new();
let mut manifest_chunks: Vec<(Hash, u64, u32)> = Vec::new();
loop {
let frame = framing::read_frame(stream)?;
match frame.ftype {
framing::T_MANIFEST_CHUNK => {
let entry = ManifestChunk::from_frame(frame)
.map_err(|e| SyncError::Protocol(format!("bad manifest chunk: {}", e)))?;
manifest.insert(entry.hash, entry.offset);
manifest_chunks.push((entry.hash, entry.offset, entry.length));
}
framing::T_MANIFEST_DONE => break,
other => {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
&format!("expected manifest frame, got 0x{:02X}", other),
));
}
}
}
let mut plan: Vec<PlanEntry> = Vec::with_capacity(source_chunks.len());
let mut remote_total: u64 = 0;
for chunk in &source_chunks {
match manifest.get(&chunk.hash) {
Some(peer_offset) => plan.push(PlanEntry::Local {
target_offset: chunk.offset,
length: chunk.length as u32,
source_offset: *peer_offset,
hash: chunk.hash,
}),
None => {
plan.push(PlanEntry::Remote {
target_offset: chunk.offset,
length: chunk.length as u32,
hash: chunk.hash,
});
remote_total += chunk.length as u64;
}
}
}
let layout_reproduced = !plan.is_empty()
&& plan.len() == manifest_chunks.len()
&& plan
.iter()
.zip(manifest_chunks.iter())
.all(|(e, (_, off, len))| match e {
PlanEntry::Local {
target_offset,
length,
..
} => target_offset == off && length == len,
_ => false,
});
for entry in &plan {
entry.into_frame()?.encode(stream)?;
}
message::encode_empty(framing::T_PLAN_DONE)?.encode(stream)?;
stream.flush()?;
let mut sent: u64 = 0;
let mut sent_chunks: usize = 0;
if remote_total > 0 {
let mut file = File::open(&request.path)?;
let mut chunk_buf = Vec::new();
let mut compressor = zstd::bulk::Compressor::new(3)?;
for entry in &plan {
let (target_offset, length, hash) = match entry {
PlanEntry::Remote {
target_offset,
length,
hash,
} => (*target_offset, *length as usize, *hash),
_ => continue,
};
chunk_buf.clear();
chunk_buf.resize(length, 0);
use std::io::{Read as _, Seek as _};
file.seek(io::SeekFrom::Start(target_offset))?;
file.read_exact(&mut chunk_buf)?;
let compressed = compressor.compress(&chunk_buf)?;
message::encode_data(hash, &compressed)?.encode(stream)?;
sent += length as u64;
sent_chunks += 1;
progress(Progress::Transferring(sent, remote_total));
}
stream.flush()?;
}
message::encode_verify(root)?.encode(stream)?;
stream.flush()?;
let frame = framing::read_frame(stream)?;
match frame.ftype {
framing::T_COMMIT_ACK => {
let index = FileIndex::from_scan(
source_chunks,
file_size,
crate::index::stat_mtime(&request.path).unwrap_or((0, 0)),
);
let _ = crate::index::write_index(&request.path, &index);
if plan.is_empty() || layout_reproduced {
Ok(SyncOutcome::InSync)
} else {
Ok(SyncOutcome::Transferred {
bytes: sent,
chunks: sent_chunks,
})
}
}
framing::T_ERROR => {
let (code, msg) = message::read_error_payload(&frame)?;
if warmed && code == ErrorCode::HashMismatch {
crate::index::remove_index(&request.path);
}
Err(SyncError::Peer(code, msg))
}
other => Err(SyncError::Protocol(format!(
"expected COMMIT_ACK, got 0x{:02X}",
other
))),
}
}
pub fn receive<S>(
stream: &mut S,
request: &ReceiveRequest,
pipeline: &PipelineConfig,
progress: &mut dyn FnMut(Progress),
) -> Result<SyncOutcome, SyncError>
where
S: Read + Write,
{
let temp_path = Reconstructor::temp_path_for(&request.path);
let resuming = temp_path.exists();
if let Some(parent) = request.path.parent()
&& let Ok(entries) = std::fs::read_dir(parent)
{
for entry in entries.flatten() {
let p = entry.path();
let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Some(base) = name.strip_suffix(".ffsync-index") else {
continue;
};
if base.is_empty() {
continue;
}
let target = parent.join(base);
if !target.exists() {
let _ = std::fs::remove_file(&p);
}
}
}
let local_meta = std::fs::metadata(&request.local_source).ok();
let local_size = local_meta.as_ref().map(|m| m.len()).unwrap_or(0);
let local_mtime = local_meta.as_ref().and_then(|m| m.modified().ok());
let warm_idx = if local_size > 0 && !request.force_checksum {
load_index(&request.local_source)
} else {
None
};
let (local_chunks, local_root) = match warm_idx {
Some(idx) => {
progress(Progress::WarmStart(idx.chunks.len()));
(idx.chunks, Some(idx.root))
}
None => {
if local_size > 0 {
progress(Progress::Scanning(0, local_size));
let (_stats, chunks) =
scan_with_progress(&request.local_source, pipeline, |hashed| {
progress(Progress::Scanning(hashed, local_size))
})?;
let root = chunk_root(&chunks);
(chunks, Some(root))
} else {
(Vec::new(), None)
}
}
};
let temp_chunks: Vec<Chunk> = if resuming {
let tsize = std::fs::metadata(&temp_path)?.len();
progress(Progress::Scanning(0, tsize));
let (_stats, chunks) = scan_with_progress(&temp_path, pipeline, |hashed| {
progress(Progress::Scanning(hashed, tsize))
})?;
chunks
} else {
Vec::new()
};
let mut manifest_map: HashMap<Hash, LocalOrigin> = HashMap::new();
for c in &temp_chunks {
manifest_map.entry(c.hash).or_insert(LocalOrigin::Temp {
offset: c.offset,
length: c.length,
});
}
for c in &local_chunks {
manifest_map.entry(c.hash).or_insert(LocalOrigin::OldFile {
offset: c.offset,
length: c.length,
});
}
let mut flags = if request.force_checksum {
0
} else {
FLAG_TRUST_TIMES
};
if resuming {
flags |= FLAG_RESUMING;
}
let hello = Hello {
version: super::VERSION,
flags,
role: Role::Receiver,
cdc_min: CHUNK_MIN as u32,
cdc_avg: CHUNK_AVG as u32,
cdc_max: CHUNK_MAX as u32,
file_size: local_size,
file_mtime: unix_secs(local_mtime),
file_mtime_nanos: local_mtime
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.subsec_nanos())
.unwrap_or(0),
path: request
.path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
};
hello.into_frame()?.encode(stream)?;
stream.flush()?;
let peer = message::read_typed::<Hello>(stream)?;
let peer = match validate_hello(&peer, Role::Receiver) {
Ok(h) => h,
Err(SyncError::Protocol(msg)) => {
return Err(send_error_and_finish(stream, ErrorCode::RoleConflict, &msg));
}
Err(SyncError::Peer(code, msg)) => {
return Err(send_error_and_finish(stream, code, &msg));
}
Err(other) => return Err(other),
};
let peer_mtime = if request.tree {
Some((peer.file_mtime, peer.file_mtime_nanos))
} else {
None
};
let request = ReceiveRequest {
apply_mtime: request.apply_mtime.or(peer_mtime),
..(*request).clone()
};
if request.tree {
let frame = framing::read_frame(stream)?;
match frame.ftype {
framing::T_NOT_MODIFIED => {
progress(Progress::NothingToDo);
return Ok(SyncOutcome::InSync);
}
framing::T_FILE_SYNC => {} framing::T_ERROR => {
let (code, msg) = message::read_error_payload(&frame)?;
return Err(SyncError::Peer(code, msg));
}
other => {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
&format!("expected NOT_MODIFIED or FILE_SYNC, got 0x{:02X}", other),
));
}
}
}
for chunk in &temp_chunks {
let frame = message::ManifestChunk {
hash: chunk.hash,
offset: chunk.offset,
length: chunk.length as u32,
}
.into_frame()?;
if frame.encode(stream).and_then(|_| stream.flush()).is_err() {
break;
}
}
let covered: HashSet<Hash> = temp_chunks.iter().map(|c| c.hash).collect();
for chunk in &local_chunks {
if covered.contains(&chunk.hash) {
continue;
}
let frame = message::ManifestChunk {
hash: chunk.hash,
offset: chunk.offset,
length: chunk.length as u32,
}
.into_frame()?;
if frame.encode(stream).and_then(|_| stream.flush()).is_err() {
break;
}
}
let manifest_done = message::encode_empty(framing::T_MANIFEST_DONE)?;
if manifest_done
.encode(stream)
.and_then(|_| stream.flush())
.is_err()
{
}
let _ = stream.flush();
let mut plan: Vec<PlanEntry> = Vec::new();
let mut remote_total: u64 = 0;
loop {
let frame = framing::read_frame(stream)?;
match frame.ftype {
framing::T_PLAN_LOCAL | framing::T_PLAN_REMOTE => {
let entry = PlanEntry::from_frame(frame)
.map_err(|e| SyncError::Protocol(format!("bad plan entry: {}", e)))?;
if let PlanEntry::Remote { length, .. } = entry {
remote_total += length as u64;
}
plan.push(entry);
}
framing::T_PLAN_DONE => break,
framing::T_NOT_MODIFIED => {
progress(Progress::NothingToDo);
return Ok(SyncOutcome::InSync);
}
framing::T_ERROR => {
let (code, msg) = message::read_error_payload(&frame)?;
return Err(SyncError::Peer(code, msg));
}
other => {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
&format!("expected plan frame, got 0x{:02X}", other),
));
}
}
}
let target_size = peer.file_size;
let plan_reproduces_local_layout = !plan.is_empty()
&& plan.iter().all(|e| matches!(e, PlanEntry::Local { .. }))
&& plan.len() == local_chunks.len()
&& plan.iter().zip(local_chunks.iter()).all(|(e, c)| match e {
PlanEntry::Local {
target_offset,
length,
..
} => *target_offset == c.offset && *length as usize == c.length,
_ => false,
});
if !plan.is_empty() && !plan_reproduces_local_layout {
let reconstructor = if resuming {
Reconstructor::open_existing(&request.path, &request.local_source, target_size)?
} else {
Reconstructor::new(&request.path, &request.local_source, target_size)?
};
let mut scratch: Vec<u8> = Vec::new();
let mut received: u64 = 0;
for entry in &plan {
match *entry {
PlanEntry::Local {
target_offset,
length,
source_offset: _,
hash,
} => {
let origin = manifest_map.get(&hash).ok_or_else(|| {
send_error_and_finish(
stream,
ErrorCode::BadFrame,
"plan references a chunk not in our manifest",
)
})?;
let (path, offset) = match origin {
LocalOrigin::Temp { offset, .. } => (&temp_path, *offset),
LocalOrigin::OldFile { offset, .. } => (&request.local_source, *offset),
};
if target_offset + length as u64 > target_size {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
"plan chunk extends beyond the declared file size",
));
}
let source_file = File::open(path)?;
scratch.clear();
scratch.resize(length as usize, 0);
source_file.read_at(&mut scratch[..length as usize], offset)?;
let actual = blake3::hash(&scratch[..length as usize]);
if actual != hash {
if matches!(origin, LocalOrigin::Temp { .. }) {
let _ = std::fs::remove_file(&temp_path);
}
crate::index::remove_index(&request.local_source);
return Err(send_error_and_finish(
stream,
ErrorCode::HashMismatch,
"local chunk content does not match its hash",
));
}
let task = ReconstructionTask {
target_offset,
source: ChunkSource {
offset: 0,
length: length as usize,
},
};
reconstructor.write_chunk(&task, &scratch)?;
}
PlanEntry::Remote {
target_offset,
length,
hash,
} => {
let (data_hash, compressed) = message::read_data(stream)?;
if data_hash != hash {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
"DATA hash does not match plan",
));
}
let decompressed = DECOMPRESSOR.with(|d| {
let mut decompressor = d.borrow_mut();
decompressor.decompress(&compressed, length as usize)
})?;
if decompressed.len() != length as usize {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
"decompressed length mismatch",
));
}
if target_offset + length as u64 > target_size {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
"plan chunk extends beyond the declared file size",
));
}
let task = ReconstructionTask {
target_offset,
source: ChunkSource {
offset: 0,
length: length as usize,
},
};
reconstructor.write_chunk(&task, &decompressed)?;
received += length as u64;
progress(Progress::Receiving(received, remote_total));
}
}
}
progress(Progress::Verifying);
let verify_root = message::read_verify(stream)?;
let temp_chunks = scan_file_plain(&temp_path, pipeline)?;
let temp_root = chunk_root(&temp_chunks);
if temp_root != verify_root {
let _ = std::fs::remove_file(&temp_path);
return Err(send_error_and_finish(
stream,
ErrorCode::HashMismatch,
"reconstructed file hash mismatch",
));
}
std::fs::rename(&temp_path, &request.path)?;
apply_commit_mtime(&request)?;
let _ = crate::index::write_index(
&request.path,
&FileIndex::from_scan(
temp_chunks,
target_size,
crate::index::stat_mtime(&request.path).unwrap_or((0, 0)),
),
);
message::encode_empty(framing::T_COMMIT_ACK)?.encode(stream)?;
stream.flush()?;
return Ok(SyncOutcome::Transferred {
bytes: received,
chunks: plan.len(),
});
}
let verify_root = message::read_verify(stream)?;
if target_size == 0 {
if verify_root != chunk_root(&[]) {
return Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
"sender reports zero size but a non-empty root",
));
}
if resuming {
let _ = std::fs::remove_file(&temp_path);
}
std::fs::File::create(&request.path)?.set_len(0)?;
apply_commit_mtime(&request)?;
let _ = crate::index::write_index(
&request.path,
&FileIndex::from_scan(
Vec::new(),
0,
crate::index::stat_mtime(&request.path).unwrap_or((0, 0)),
),
);
message::encode_empty(framing::T_COMMIT_ACK)?.encode(stream)?;
stream.flush()?;
progress(Progress::NothingToDo);
return Ok(SyncOutcome::Transferred {
bytes: 0,
chunks: 0,
});
}
let (state_root, state_desc): (Option<Hash>, &str) = if resuming {
let chunks = scan_file_plain(&temp_path, pipeline)?;
(Some(chunk_root(&chunks)), "temp")
} else {
(local_root, "old file")
};
match state_root {
Some(root) if root == verify_root => {
if resuming {
std::fs::rename(&temp_path, &request.path)?;
apply_commit_mtime(&request)?;
let chunks = scan_file_plain(&temp_path, pipeline).unwrap_or_default();
let _ = crate::index::write_index(
&request.path,
&FileIndex::from_scan(
chunks,
target_size,
crate::index::stat_mtime(&request.path).unwrap_or((0, 0)),
),
);
} else if request.local_source == request.path {
apply_commit_mtime(&request)?;
let _ = crate::index::write_index(
&request.path,
&FileIndex::from_scan(
local_chunks.clone(),
local_size,
crate::index::stat_mtime(&request.local_source).unwrap_or((0, 0)),
),
);
}
message::encode_empty(framing::T_COMMIT_ACK)?.encode(stream)?;
stream.flush()?;
progress(Progress::NothingToDo);
Ok(SyncOutcome::InSync)
}
Some(_) => Err(send_error_and_finish(
stream,
ErrorCode::HashMismatch,
&format!(
"content differs despite empty plan (stale {} manifest?)",
state_desc
),
)),
None => {
if target_size == 0 {
if resuming {
std::fs::remove_file(&temp_path).ok();
}
std::fs::File::create(&request.path)?.set_len(0)?;
apply_commit_mtime(&request)?;
let _ = crate::index::write_index(
&request.path,
&FileIndex::from_scan(
Vec::new(),
0,
crate::index::stat_mtime(&request.path).unwrap_or((0, 0)),
),
);
message::encode_empty(framing::T_COMMIT_ACK)?.encode(stream)?;
stream.flush()?;
progress(Progress::NothingToDo);
return Ok(SyncOutcome::Transferred {
bytes: 0,
chunks: 0,
});
}
Err(send_error_and_finish(
stream,
ErrorCode::BadFrame,
"empty plan without a local file",
))
}
}
}
fn apply_commit_mtime(request: &ReceiveRequest) -> io::Result<()> {
let Some((secs, nanos)) = request.apply_mtime else {
return Ok(());
};
if secs == 0 && nanos == 0 {
return Ok(()); }
let f = std::fs::OpenOptions::new()
.write(true)
.open(&request.path)?;
f.set_modified(UNIX_EPOCH + std::time::Duration::new(secs, nanos))
}
fn scan_with_progress(
path: &Path,
pipeline: &PipelineConfig,
mut on_progress: impl FnMut(u64),
) -> Result<(u64, Vec<Chunk>), SyncError> {
let rx = process_file(
path,
move || FastCDCChunker::new(CHUNK_MIN, CHUNK_AVG, CHUNK_MAX),
Blake3Hasher,
pipeline,
)?;
let mut hashed: u64 = 0;
let mut chunks = Vec::new();
loop {
match rx.recv() {
Ok(ChunkEvent::Chunk(chunk)) => {
hashed += chunk.length as u64;
chunks.push(chunk);
on_progress(hashed);
}
Ok(ChunkEvent::Done) => {}
Err(_) => break,
}
}
chunks.sort_by_key(|c| c.offset);
Ok((hashed, chunks))
}
fn scan_file_plain(path: &Path, pipeline: &PipelineConfig) -> Result<Vec<Chunk>, SyncError> {
let (_, chunks) = scan_with_progress(path, pipeline, |_| {})?;
Ok(chunks)
}