1use std::collections::{HashMap, HashSet};
10use std::io::{Read, Write};
11use std::path::{Path, PathBuf};
12
13use filetime::FileTime;
14
15use crate::delta::{Signature, apply};
16use crate::filter::Filter;
17use crate::meta::{
18 canonical_root, check_relative, contained_target, set_mode, set_mtime, set_symlink_mtime,
19};
20use crate::net::proto::{Data, Msg, NetEntry, NetKind, NetOptions, Request};
21use crate::net::transport::{recv_msg, send_msg};
22use crate::plan::Action;
23use crate::report::{Event, Reporter, RunPhase, Stats};
24use crate::walk::{Entry, walk_controlled};
25use crate::{Error, Result, RunControl};
26
27#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
33pub fn run_receiver<C: Read + Write, R: Reporter>(
34 conn: &mut C,
35 root: &Path,
36 filter: &Filter,
37 options: NetOptions,
38 threads: usize,
39 control: &RunControl,
40 reporter: &R,
41) -> Result<Stats> {
42 reporter.event(Event::Phase(RunPhase::Copying));
43 let root_canon = canonical_root(root)?;
44
45 let mut src_entries: Vec<NetEntry> = Vec::new();
47 loop {
48 control.checkpoint()?;
49 match recv_msg(conn)? {
50 Msg::Entry(e) => src_entries.push(e),
51 Msg::ListDone => break,
52 Msg::Error(e) => return Err(Error::Protocol(format!("peer error: {e}"))),
53 other => {
54 return Err(Error::Protocol(format!(
55 "receiver: expected entry/list-done, got {other:?}"
56 )));
57 }
58 }
59 }
60
61 let existing = walk_controlled(root, threads, filter, control)?;
63 let existing_map: HashMap<PathBuf, Entry> = existing
64 .iter()
65 .map(|e| (e.rel.clone(), e.clone()))
66 .collect();
67 let src_set: HashSet<&PathBuf> = src_entries.iter().map(|e| &e.rel).collect();
68
69 let mut stats = Stats::default();
70
71 for e in &src_entries {
73 control.checkpoint()?;
74 if check_relative(&e.rel).is_err() {
75 fail(
76 reporter,
77 &mut stats,
78 &e.rel,
79 "path escapes destination root",
80 );
81 continue;
82 }
83 let mtime = FileTime::from_unix_time(e.mtime_s, e.mtime_ns);
84 let existing_entry = existing_map.get(&e.rel);
85 match &e.kind {
86 NetKind::Dir => {
87 let full = root_canon.join(&e.rel);
88 if let Err(err) = std::fs::create_dir_all(&full) {
89 fail(reporter, &mut stats, &e.rel, &err.to_string());
90 continue;
91 }
92 apply_meta(&full, e.mode, mtime, options, false);
93 let action = bump(&mut stats, existing_entry.is_some());
94 reporter.event(Event::DirDone {
95 rel: e.rel.clone(),
96 action,
97 });
98 }
99 NetKind::Symlink(target) => {
100 let dst = match ensure_and_contain(&root_canon, &e.rel) {
101 Ok(p) => p,
102 Err(err) => {
103 fail(reporter, &mut stats, &e.rel, &err.to_string());
104 continue;
105 }
106 };
107 if let Ok(meta) = std::fs::symlink_metadata(&dst) {
109 if meta.is_dir() {
110 let _ = std::fs::remove_dir_all(&dst);
111 } else {
112 let _ = std::fs::remove_file(&dst);
113 }
114 }
115 #[cfg(unix)]
117 {
118 use std::path::Component;
119 if target.is_absolute()
120 || target.components().any(|c| c == Component::ParentDir)
121 {
122 fail(
123 reporter,
124 &mut stats,
125 &e.rel,
126 "symlink target escapes destination root",
127 );
128 continue;
129 }
130 }
131 if let Err(err) = symlink_create(target, &dst) {
132 fail(reporter, &mut stats, &e.rel, &err.to_string());
133 continue;
134 }
135 if options.preserve_mtime {
136 let _ = set_symlink_mtime(&dst, mtime);
137 }
138 let action = bump(&mut stats, existing_entry.is_some());
139 reporter.event(Event::SymlinkDone {
140 rel: e.rel.clone(),
141 action,
142 });
143 }
144 NetKind::File => {
145 let is_existing_file = existing_entry.is_some_and(Entry::is_file);
146 let unchanged = !options.checksum
147 && is_existing_file
148 && existing_entry.is_some_and(|x| {
149 x.len == e.len
150 && x.mtime.unix_seconds() == e.mtime_s
151 && x.mtime.nanoseconds() == e.mtime_ns
152 });
153 if unchanged {
154 stats.skipped += 1;
155 reporter.event(Event::Skipped { rel: e.rel.clone() });
156 continue;
157 }
158 let dst = match ensure_and_contain(&root_canon, &e.rel) {
159 Ok(p) => p,
160 Err(err) => {
161 fail(reporter, &mut stats, &e.rel, &err.to_string());
162 continue;
163 }
164 };
165 let Some(new_bytes) = fetch_bytes(conn, e, &dst, is_existing_file, options)? else {
166 fail(
167 reporter,
168 &mut stats,
169 &e.rel,
170 "source unreadable or delta failed",
171 );
172 continue;
173 };
174 if let Err(err) = write_atomic(&dst, &new_bytes, e.mode, mtime, options) {
175 fail(reporter, &mut stats, &e.rel, &err.to_string());
176 continue;
177 }
178 stats.bytes += new_bytes.len() as u64;
179 let action = bump(&mut stats, is_existing_file);
180 reporter.event(Event::FileDone {
181 rel: e.rel.clone(),
182 action,
183 bytes: new_bytes.len() as u64,
184 });
185 }
186 }
187 }
188
189 if options.delete {
191 let mut victims: Vec<&Entry> = existing
192 .iter()
193 .filter(|x| !src_set.contains(&x.rel))
194 .collect();
195 victims.sort_by(|a, b| b.rel.cmp(&a.rel));
196 for victim in victims {
197 control.checkpoint()?;
198 let full = root_canon.join(&victim.rel);
199 let removed = if victim.is_dir() {
200 std::fs::remove_dir(&full).or_else(|_| std::fs::remove_dir_all(&full))
201 } else {
202 std::fs::remove_file(&full)
203 };
204 if removed.is_ok() {
205 stats.deleted += 1;
206 reporter.event(Event::Deleted {
207 rel: victim.rel.clone(),
208 });
209 }
210 }
211 }
212
213 send_msg(conn, &Msg::Finished(stats.clone()))?;
215 Ok(stats)
216}
217
218fn fetch_bytes<C: Read + Write>(
222 conn: &mut C,
223 e: &NetEntry,
224 dst: &Path,
225 is_existing_file: bool,
226 options: NetOptions,
227) -> Result<Option<Vec<u8>>> {
228 let use_delta = !options.whole_file && is_existing_file;
229 if use_delta {
230 let old = std::fs::read(dst).unwrap_or_default();
231 let sig = Signature::compute(&old, None);
232 send_msg(conn, &Msg::Request(Request::Delta(e.rel.clone(), sig)))?;
233 match recv_msg(conn)? {
234 Msg::Data(Data::Delta(d)) => Ok(apply(&old, &d).map_err(|e| tracing::debug!("failed to apply delta: {e}")).ok()),
235 Msg::Data(Data::Whole { bytes, zstd }) => Ok(unpack(bytes, zstd)),
236 Msg::Data(Data::NotFound) => Ok(None),
237 Msg::Error(e) => Err(Error::Protocol(format!("peer error: {e}"))),
238 other => Err(Error::Protocol(format!("receiver: unexpected {other:?}"))),
239 }
240 } else {
241 send_msg(conn, &Msg::Request(Request::Whole(e.rel.clone())))?;
242 match recv_msg(conn)? {
243 Msg::Data(Data::Whole { bytes, zstd }) => Ok(unpack(bytes, zstd)),
244 Msg::Data(Data::NotFound) => Ok(None),
245 Msg::Error(e) => Err(Error::Protocol(format!("peer error: {e}"))),
246 other => Err(Error::Protocol(format!("receiver: unexpected {other:?}"))),
247 }
248 }
249}
250
251fn unpack(bytes: Vec<u8>, zstd: bool) -> Option<Vec<u8>> {
254 if zstd {
255 zstd::decode_all(bytes.as_slice()).map_err(|e| tracing::debug!("failed to decompress: {e}")).ok()
256 } else {
257 Some(bytes)
258 }
259}
260
261fn fail<R: Reporter>(reporter: &R, stats: &mut Stats, rel: &Path, error: &str) {
263 stats.errors += 1;
264 reporter.event(Event::Failed {
265 rel: rel.to_path_buf(),
266 error: error.to_string(),
267 });
268}
269
270fn bump(stats: &mut Stats, existed: bool) -> Action {
272 if existed {
273 stats.updated += 1;
274 Action::Update
275 } else {
276 stats.copied += 1;
277 Action::Copy
278 }
279}
280
281fn apply_meta(path: &Path, mode: u32, mtime: FileTime, options: NetOptions, _is_file: bool) {
283 if options.preserve_mode {
284 let _ = set_mode(path, mode);
285 }
286 if options.preserve_mtime {
287 let _ = set_mtime(path, mtime);
288 }
289}
290
291fn ensure_and_contain(root_canon: &Path, rel: &Path) -> Result<PathBuf> {
294 let target = root_canon.join(rel);
295 if let Some(parent) = target.parent() {
296 std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
297 }
298 contained_target(root_canon, &target)
299}
300
301fn write_atomic(
303 dst: &Path,
304 bytes: &[u8],
305 mode: u32,
306 mtime: FileTime,
307 options: NetOptions,
308) -> Result<()> {
309 let parent = dst
310 .parent()
311 .ok_or_else(|| Error::Containment(dst.to_path_buf()))?;
312 let tmp = parent.join(format!(".ripsync.tmp.{:016x}", rand::random::<u64>()));
313 std::fs::write(&tmp, bytes).map_err(|e| Error::io(&tmp, e))?;
314 apply_meta(&tmp, mode, mtime, options, true);
315
316 if let Ok(meta) = std::fs::symlink_metadata(dst) {
318 if meta.is_dir() {
319 if let Err(e) = std::fs::remove_dir_all(dst) {
320 eprintln!("Warning: failed to remove directory {:?}: {}", dst, e);
322 }
323 }
324 }
325
326 if let Err(e) = atomic_replace(&tmp, dst) {
327 let _ = std::fs::remove_file(&tmp);
328 return Err(Error::io(dst, e));
329 }
330 Ok(())
331}
332
333#[cfg(not(windows))]
335fn atomic_replace(tmp: &Path, dst: &Path) -> std::io::Result<()> {
336 std::fs::rename(tmp, dst)
337}
338
339#[cfg(windows)]
340fn atomic_replace(tmp: &Path, dst: &Path) -> std::io::Result<()> {
341 crate::io::windows::replace_file(tmp, dst)
342}
343
344#[cfg(unix)]
346fn symlink_create(target: &Path, link: &Path) -> std::io::Result<()> {
347 std::os::unix::fs::symlink(target, link)
348}
349
350#[cfg(windows)]
351fn symlink_create(target: &Path, link: &Path) -> std::io::Result<()> {
352 std::os::windows::fs::symlink_file(target, link)
354}