1use std::collections::HashMap;
10use std::io;
11use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use rayon::prelude::*;
15
16use crate::control::RunControl;
17use crate::copy::{FsyncMode, ReflinkMode, copy_file_into, copy_file_into_sized};
18use crate::meta::{
19 canonical_root, check_relative, contained_target, copy_xattrs, set_mode, set_mtime,
20 set_owner_group, set_symlink_mtime,
21};
22use crate::plan::{Action, SyncPlan};
23use crate::report::{Event, Reporter, RunPhase, Stats};
24use crate::walk::{Entry, EntryKind};
25use crate::{Error, Result};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum Backend {
30 #[default]
32 Auto,
33 Uring,
35 Portable,
37}
38
39#[derive(Debug, Clone, Copy, Default)]
41pub struct ApplyOptions {
42 pub dry_run: bool,
44 pub yes: bool,
46 pub delete: bool,
48 pub threads: usize,
50 pub reflink: ReflinkMode,
52 pub fsync: FsyncMode,
54 pub backend: Backend,
56 pub metadata: MetadataOptions,
58 pub copy_buffer: Option<usize>,
61}
62
63#[derive(Debug, Clone, Copy, Default)]
65#[allow(clippy::struct_excessive_bools)]
66pub struct MetadataOptions {
67 pub hard_links: bool,
69 pub sparse: bool,
71 pub xattrs: bool,
73 pub acls: bool,
75 pub owner: bool,
77 pub group: bool,
79}
80
81#[derive(Default)]
83struct Counters {
84 copied: AtomicU64,
85 updated: AtomicU64,
86 skipped: AtomicU64,
87 deleted: AtomicU64,
88 errors: AtomicU64,
89 bytes: AtomicU64,
90}
91
92impl Counters {
93 fn snapshot(&self) -> Stats {
94 Stats {
95 copied: self.copied.load(Ordering::Relaxed),
96 updated: self.updated.load(Ordering::Relaxed),
97 skipped: self.skipped.load(Ordering::Relaxed),
98 deleted: self.deleted.load(Ordering::Relaxed),
99 errors: self.errors.load(Ordering::Relaxed),
100 bytes: self.bytes.load(Ordering::Relaxed),
101 }
102 }
103 fn bump_action(&self, action: Action) {
104 match action {
105 Action::Copy => self.copied.fetch_add(1, Ordering::Relaxed),
106 Action::Update => self.updated.fetch_add(1, Ordering::Relaxed),
107 Action::Skip => self.skipped.fetch_add(1, Ordering::Relaxed),
108 };
109 }
110}
111
112pub fn apply_plan<R: Reporter>(
123 plan: &SyncPlan,
124 src: &Path,
125 dst: &Path,
126 opts: ApplyOptions,
127 reporter: &R,
128) -> Result<Stats> {
129 apply_plan_controlled(plan, src, dst, opts, reporter, &RunControl::default())
130}
131
132pub fn apply_plan_controlled<R: Reporter>(
139 plan: &SyncPlan,
140 src: &Path,
141 dst: &Path,
142 opts: ApplyOptions,
143 reporter: &R,
144 control: &RunControl,
145) -> Result<Stats> {
146 control.checkpoint()?;
147 reporter.event(Event::Planned {
148 total_files: plan
149 .actions
150 .iter()
151 .filter(|a| a.action != Action::Skip && a.entry.is_file())
152 .count(),
153 total_bytes: plan.bytes_to_transfer(),
154 deletions: plan.deletions.len(),
155 });
156
157 let counters = Counters::default();
158
159 if opts.dry_run {
160 for pa in &plan.actions {
161 control.checkpoint()?;
162 emit_planned_event(reporter, &pa.entry, pa.action);
163 counters.bump_action(pa.action);
164 if pa.action != Action::Skip && pa.entry.is_file() {
165 counters.bytes.fetch_add(pa.entry.len, Ordering::Relaxed);
166 }
167 }
168 for del in &plan.deletions {
169 control.checkpoint()?;
170 reporter.event(Event::Deleted {
171 rel: del.rel.clone(),
172 });
173 counters.deleted.fetch_add(1, Ordering::Relaxed);
174 }
175 return Ok(counters.snapshot());
176 }
177
178 apply_real(plan, src, dst, opts, reporter, &counters, control)?;
179 Ok(counters.snapshot())
180}
181
182#[allow(clippy::too_many_lines)]
184fn apply_real<R: Reporter>(
185 plan: &SyncPlan,
186 src: &Path,
187 dst: &Path,
188 opts: ApplyOptions,
189 reporter: &R,
190 counters: &Counters,
191 control: &RunControl,
192) -> Result<()> {
193 reporter.event(Event::Phase(RunPhase::Copying));
194 control.checkpoint()?;
195 let root_canon = canonical_root(dst)?;
196
197 let mut dirs: Vec<&Entry> = Vec::new();
200 let mut mtime_dirs: Vec<&Entry> = Vec::new();
201 let mut files: Vec<(&Entry, Action)> = Vec::new();
202 let mut hardlinks: Vec<(&Entry, Action, PathBuf)> = Vec::new();
203 let mut symlinks: Vec<(&Entry, Action)> = Vec::new();
204 let mut first_hardlink: HashMap<(u64, u64), PathBuf> = HashMap::new();
205
206 for pa in &plan.actions {
207 if pa.entry.is_dir() {
208 mtime_dirs.push(&pa.entry);
209 }
210 if opts.metadata.hard_links && pa.entry.is_file() {
211 let id = (pa.entry.dev, pa.entry.ino);
212 if let Some(canonical) = first_hardlink.get(&id) {
213 if pa.action == Action::Skip {
214 reporter.event(Event::Skipped {
215 rel: pa.entry.rel.clone(),
216 });
217 counters.skipped.fetch_add(1, Ordering::Relaxed);
218 } else {
219 check_relative(&pa.entry.rel)?;
220 hardlinks.push((&pa.entry, pa.action, canonical.clone()));
221 }
222 continue;
223 }
224 first_hardlink.insert(id, pa.entry.rel.clone());
225 }
226 if pa.action == Action::Skip {
227 reporter.event(Event::Skipped {
228 rel: pa.entry.rel.clone(),
229 });
230 counters.skipped.fetch_add(1, Ordering::Relaxed);
231 continue;
232 }
233 check_relative(&pa.entry.rel)?;
234 match pa.entry.kind {
235 EntryKind::Dir => dirs.push(&pa.entry),
236 EntryKind::File => files.push((&pa.entry, pa.action)),
237 EntryKind::Symlink(_) => symlinks.push((&pa.entry, pa.action)),
238 }
239 }
240
241 let (effective_backend, backend_name, backend_reason) = resolve_backend(opts, &files);
244 reporter.event(Event::BackendSelected {
245 backend: backend_name,
246 reason: backend_reason,
247 });
248 let mut copy_opts = opts;
249 copy_opts.backend = effective_backend;
250
251 let pool = build_pool(opts.threads);
252
253 control.checkpoint()?;
255 let run_dirs = || {
256 create_directories(
257 &dirs,
258 src,
259 dst,
260 &root_canon,
261 opts,
262 reporter,
263 counters,
264 control,
265 )
266 };
267 match &pool {
268 Some(thread_pool) => thread_pool.install(run_dirs)?,
269 None => run_dirs()?,
270 }
271
272 control.checkpoint()?;
274 let run_files = || {
275 copy_files(
276 &files,
277 src,
278 &root_canon,
279 copy_opts,
280 reporter,
281 counters,
282 control,
283 )
284 };
285 match &pool {
286 Some(p) => p.install(run_files)?,
287 None => run_files()?,
288 }
289
290 copy_hardlinks(&hardlinks, &root_canon, reporter, counters, control)?;
292
293 for (entry, action) in &symlinks {
295 control.checkpoint()?;
296 if let EntryKind::Symlink(link_target) = &entry.kind {
297 match create_symlink(src, &root_canon, entry, link_target, opts) {
298 Ok(()) => {
299 reporter.event(Event::SymlinkDone {
300 rel: entry.rel.clone(),
301 action: *action,
302 });
303 counters.bump_action(*action);
304 }
305 Err(e) => report_fail(reporter, counters, &entry.rel, &e),
306 }
307 }
308 }
309
310 for entry in mtime_dirs.iter().rev() {
312 control.checkpoint()?;
313 let target = root_canon.join(&entry.rel);
314 let _ = set_mtime(&target, entry.mtime);
315 }
316
317 if opts.delete && opts.yes {
319 reporter.event(Event::Phase(RunPhase::Deleting));
320 run_deletions(plan, &root_canon, reporter, counters, control)?;
321 }
322
323 if opts.fsync == FsyncMode::Auto {
326 control.checkpoint()?;
327 fsync_touched_dirs(&root_canon, &dirs, &files);
328 }
329
330 control.checkpoint()?;
331 Ok(())
332}
333
334#[allow(clippy::too_many_arguments)]
335fn create_directories<R: Reporter>(
336 dirs: &[&Entry],
337 src: &Path,
338 dst: &Path,
339 root_canon: &Path,
340 opts: ApplyOptions,
341 reporter: &R,
342 counters: &Counters,
343 control: &RunControl,
344) -> Result<()> {
345 let mut start = 0;
346 while start < dirs.len() {
347 control.checkpoint()?;
348 let depth = dirs[start].rel.components().count();
349 let end = dirs[start..]
350 .iter()
351 .position(|entry| entry.rel.components().count() != depth)
352 .map_or(dirs.len(), |offset| start + offset);
353 dirs[start..end].par_iter().for_each(|entry| {
354 if control.is_cancelled() {
355 return;
356 }
357 create_directory(entry, src, dst, root_canon, opts, reporter, counters);
358 });
359 control.checkpoint()?;
360 start = end;
361 }
362 Ok(())
363}
364
365fn create_directory<R: Reporter>(
366 entry: &Entry,
367 src: &Path,
368 dst: &Path,
369 root_canon: &Path,
370 opts: ApplyOptions,
371 reporter: &R,
372 counters: &Counters,
373) {
374 let action = action_for_existing(dst, entry);
375 let target = root_canon.join(&entry.rel);
376 if let Ok(meta) = std::fs::symlink_metadata(&target) {
377 if !meta.is_dir() {
378 if let Err(error) =
379 std::fs::remove_file(&target).map_err(|error| Error::io(&target, error))
380 {
381 report_fail(reporter, counters, &entry.rel, &error);
382 return;
383 }
384 }
385 }
386 if let Err(error) = std::fs::create_dir_all(&target).map_err(|error| Error::io(&target, error))
387 {
388 report_fail(reporter, counters, &entry.rel, &error);
389 return;
390 }
391 let result = contained_target(root_canon, &target).and_then(|contained| {
392 let source = src.join(&entry.rel);
393 set_owner_group(
394 &contained,
395 entry.uid,
396 entry.gid,
397 opts.metadata.owner,
398 opts.metadata.group,
399 true,
400 )?;
401 set_mode(&contained, entry.mode)?;
402 copy_xattrs(
403 &source,
404 &contained,
405 opts.metadata.xattrs,
406 opts.metadata.acls,
407 )
408 });
409 if let Err(error) = result {
410 report_fail(reporter, counters, &entry.rel, &error);
411 return;
412 }
413 reporter.event(Event::DirDone {
414 rel: entry.rel.clone(),
415 action,
416 });
417 counters.bump_action(action);
418}
419
420fn fsync_touched_dirs(root_canon: &Path, dirs: &[&Entry], files: &[(&Entry, Action)]) {
423 let mut targets: std::collections::HashSet<std::path::PathBuf> =
424 std::collections::HashSet::new();
425 targets.insert(root_canon.to_path_buf());
426 for entry in dirs {
427 targets.insert(root_canon.join(&entry.rel));
428 }
429 for (entry, _) in files {
430 if let Some(parent) = root_canon.join(&entry.rel).parent() {
431 targets.insert(parent.to_path_buf());
432 }
433 }
434 for dir in targets {
435 if let Ok(f) = std::fs::File::open(&dir) {
436 let _ = f.sync_all();
437 }
438 }
439}
440
441fn run_deletions<R: Reporter>(
443 plan: &SyncPlan,
444 root_canon: &Path,
445 reporter: &R,
446 counters: &Counters,
447 control: &RunControl,
448) -> Result<()> {
449 for del in &plan.deletions {
450 control.checkpoint()?;
451 match delete_entry(root_canon, &del.rel, del.is_dir) {
452 Ok(()) => {
453 reporter.event(Event::Deleted {
454 rel: del.rel.clone(),
455 });
456 counters.deleted.fetch_add(1, Ordering::Relaxed);
457 }
458 Err(e) => report_fail(reporter, counters, &del.rel, &e),
459 }
460 }
461 Ok(())
462}
463
464fn build_pool(threads: usize) -> Option<rayon::ThreadPool> {
465 if threads == 0 {
466 None
467 } else {
468 rayon::ThreadPoolBuilder::new()
469 .num_threads(threads)
470 .build()
471 .map_err(|e| tracing::warn!("failed to create thread pool: {e}"))
472 .ok()
473 }
474}
475
476fn action_for_existing(dst: &Path, entry: &Entry) -> Action {
477 if dst.join(&entry.rel).exists() {
478 Action::Update
479 } else {
480 Action::Copy
481 }
482}
483
484fn emit_planned_event<R: Reporter>(reporter: &R, entry: &Entry, action: Action) {
485 if action == Action::Skip {
486 reporter.event(Event::Skipped {
487 rel: entry.rel.clone(),
488 });
489 return;
490 }
491 match entry.kind {
492 EntryKind::Dir => reporter.event(Event::DirDone {
493 rel: entry.rel.clone(),
494 action,
495 }),
496 EntryKind::File => reporter.event(Event::FileDone {
497 rel: entry.rel.clone(),
498 action,
499 bytes: entry.len,
500 }),
501 EntryKind::Symlink(_) => reporter.event(Event::SymlinkDone {
502 rel: entry.rel.clone(),
503 action,
504 }),
505 }
506}
507
508fn report_fail<R: Reporter>(reporter: &R, counters: &Counters, rel: &Path, err: &Error) {
509 counters.errors.fetch_add(1, Ordering::Relaxed);
510 reporter.event(Event::Failed {
511 rel: rel.to_path_buf(),
512 error: err.to_string(),
513 });
514}
515
516fn prepare_paths(
518 root_canon: &Path,
519 entry: &Entry,
520) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
521 let dst_path = root_canon.join(&entry.rel);
522 let target = contained_target(root_canon, &dst_path)?;
523 let parent = target
524 .parent()
525 .ok_or_else(|| Error::Containment(target.clone()))?;
526 let tmp = parent.join(format!(".ripsync-tmp-{:016x}", rand::random::<u64>()));
527 Ok((target, tmp))
528}
529
530fn finalize_file(
532 src: &Path,
533 target: &Path,
534 tmp: &Path,
535 entry: &Entry,
536 opts: ApplyOptions,
537) -> Result<()> {
538 set_owner_group(
539 tmp,
540 entry.uid,
541 entry.gid,
542 opts.metadata.owner,
543 opts.metadata.group,
544 true,
545 )?;
546 set_mode(tmp, entry.mode)?;
547 copy_xattrs(src, tmp, opts.metadata.xattrs, opts.metadata.acls)?;
548 set_mtime(tmp, entry.mtime)?;
549 if opts.fsync == FsyncMode::Always {
550 if let Ok(f) = std::fs::File::open(tmp) {
551 let _ = f.sync_all();
552 }
553 }
554 if let Ok(meta) = std::fs::symlink_metadata(target) {
556 if meta.is_dir() {
557 if let Err(e) = std::fs::remove_dir_all(target) {
558 let _ = std::fs::remove_file(tmp);
559 return Err(Error::io(target, e));
560 }
561 }
562 }
563 if let Err(e) = atomic_replace(tmp, target) {
564 let _ = std::fs::remove_file(tmp);
565 return Err(Error::io(target, e));
566 }
567 Ok(())
568}
569
570#[cfg(not(windows))]
574fn atomic_replace(tmp: &Path, target: &Path) -> io::Result<()> {
575 std::fs::rename(tmp, target)
576}
577
578#[cfg(windows)]
579fn atomic_replace(tmp: &Path, target: &Path) -> io::Result<()> {
580 crate::io::windows::replace_file(tmp, target)
581}
582
583fn copy_file_atomic(
585 src: &Path,
586 root_canon: &Path,
587 entry: &Entry,
588 opts: ApplyOptions,
589) -> Result<u64> {
590 let src_path = src.join(&entry.rel);
591 let (target, tmp) = prepare_paths(root_canon, entry)?;
592 let bytes = match opts.copy_buffer {
594 Some(buf_size) => copy_file_into_sized(&src_path, &tmp, opts.reflink, opts.metadata.sparse, buf_size),
595 None => copy_file_into(&src_path, &tmp, opts.reflink, opts.metadata.sparse),
596 };
597 let bytes = match bytes {
598 Ok(b) => b,
599 Err(e) => {
600 let _ = std::fs::remove_file(&tmp);
601 return Err(Error::io(&src_path, e));
602 }
603 };
604 finalize_file(&src_path, &target, &tmp, entry, opts)?;
605 Ok(bytes)
606}
607
608fn copy_files<R: Reporter>(
610 files: &[(&Entry, Action)],
611 src: &Path,
612 root_canon: &Path,
613 opts: ApplyOptions,
614 reporter: &R,
615 counters: &Counters,
616 control: &RunControl,
617) -> Result<()> {
618 #[cfg(all(target_os = "linux", feature = "io-uring"))]
619 {
620 if opts.backend == Backend::Uring && !opts.metadata.sparse {
621 control.checkpoint()?;
622 copy_files_uring(files, src, root_canon, opts, reporter, counters);
623 control.checkpoint()?;
624 return Ok(());
625 }
626 }
627 copy_files_portable(files, src, root_canon, opts, reporter, counters, control)
628}
629
630fn copy_files_portable<R: Reporter>(
632 files: &[(&Entry, Action)],
633 src: &Path,
634 root_canon: &Path,
635 opts: ApplyOptions,
636 reporter: &R,
637 counters: &Counters,
638 control: &RunControl,
639) -> Result<()> {
640 let chunk_size = opts.threads.max(1).saturating_mul(2);
641 for chunk in files.chunks(chunk_size) {
642 control.checkpoint()?;
643 chunk.par_iter().for_each(|(entry, action)| {
644 if control.is_cancelled() {
645 return;
646 }
647 reporter.event(Event::FileStart {
648 rel: entry.rel.clone(),
649 len: entry.len,
650 });
651 match copy_file_atomic(src, root_canon, entry, opts) {
652 Ok(bytes) => {
653 counters.bytes.fetch_add(bytes, Ordering::Relaxed);
654 counters.bump_action(*action);
655 reporter.event(Event::FileDone {
656 rel: entry.rel.clone(),
657 action: *action,
658 bytes,
659 });
660 }
661 Err(e) => report_fail(reporter, counters, &entry.rel, &e),
662 }
663 });
664 control.checkpoint()?;
665 }
666 Ok(())
667}
668
669#[cfg(all(target_os = "linux", feature = "io-uring"))]
672fn copy_files_uring<R: Reporter>(
673 files: &[(&Entry, Action)],
674 src: &Path,
675 root_canon: &Path,
676 opts: ApplyOptions,
677 reporter: &R,
678 counters: &Counters,
679) {
680 use crate::io::uring::{self, Job};
681
682 struct Prepared<'a> {
684 entry: &'a Entry,
685 action: Action,
686 src_path: std::path::PathBuf,
687 target: std::path::PathBuf,
688 tmp: std::path::PathBuf,
689 }
690 let mut prepared: Vec<Prepared> = Vec::with_capacity(files.len());
691 for (entry, action) in files {
692 reporter.event(Event::FileStart {
693 rel: entry.rel.clone(),
694 len: entry.len,
695 });
696 match prepare_paths(root_canon, entry) {
697 Ok((target, tmp)) => prepared.push(Prepared {
698 entry,
699 action: *action,
700 src_path: src.join(&entry.rel),
701 target,
702 tmp,
703 }),
704 Err(e) => report_fail(reporter, counters, &entry.rel, &e),
705 }
706 }
707
708 let jobs: Vec<Job> = prepared
709 .iter()
710 .map(|p| Job {
711 src: &p.src_path,
712 tmp: &p.tmp,
713 len: p.entry.len,
714 })
715 .collect();
716 let batch = uring::copy_batch(&jobs);
717
718 prepared.par_iter().zip(batch).for_each(|(p, res)| {
720 let outcome = if let Ok(bytes) = res {
721 finalize_file(&p.src_path, &p.target, &p.tmp, p.entry, opts).map(|()| bytes)
722 } else {
723 let _ = std::fs::remove_file(&p.tmp);
725 (match opts.copy_buffer {
726 Some(buf_size) => copy_file_into_sized(&p.src_path, &p.tmp, opts.reflink, opts.metadata.sparse, buf_size),
727 None => copy_file_into(&p.src_path, &p.tmp, opts.reflink, opts.metadata.sparse),
728 })
729 .map_err(|e| Error::io(&p.src_path, e))
730 .and_then(|bytes| {
731 finalize_file(&p.src_path, &p.target, &p.tmp, p.entry, opts).map(|()| bytes)
732 })
733 };
734 match outcome {
735 Ok(bytes) => {
736 counters.bytes.fetch_add(bytes, Ordering::Relaxed);
737 counters.bump_action(p.action);
738 reporter.event(Event::FileDone {
739 rel: p.entry.rel.clone(),
740 action: p.action,
741 bytes,
742 });
743 }
744 Err(e) => report_fail(reporter, counters, &p.entry.rel, &e),
745 }
746 });
747}
748
749fn copy_hardlinks<R: Reporter>(
751 hardlinks: &[(&Entry, Action, PathBuf)],
752 root_canon: &Path,
753 reporter: &R,
754 counters: &Counters,
755 control: &RunControl,
756) -> Result<()> {
757 for (entry, action, canonical) in hardlinks {
758 control.checkpoint()?;
759 reporter.event(Event::FileStart {
760 rel: entry.rel.clone(),
761 len: entry.len,
762 });
763 match create_hardlink(root_canon, entry, canonical) {
764 Ok(()) => {
765 counters.bump_action(*action);
766 reporter.event(Event::FileDone {
767 rel: entry.rel.clone(),
768 action: *action,
769 bytes: 0,
770 });
771 }
772 Err(error) => report_fail(reporter, counters, &entry.rel, &error),
773 }
774 }
775 Ok(())
776}
777
778fn resolve_backend(
787 opts: ApplyOptions,
788 files: &[(&Entry, Action)],
789) -> (Backend, &'static str, &'static str) {
790 match opts.backend {
791 Backend::Portable => (Backend::Portable, "portable", "explicitly requested"),
792 Backend::Uring if opts.metadata.sparse => (
793 Backend::Portable,
794 "portable",
795 "sparse preservation requires portable",
796 ),
797 Backend::Uring => (Backend::Uring, "uring", "explicitly requested"),
798 Backend::Auto => resolve_auto_backend(opts, files),
799 }
800}
801
802#[cfg(all(target_os = "linux", feature = "io-uring"))]
803fn resolve_auto_backend(
804 opts: ApplyOptions,
805 files: &[(&Entry, Action)],
806) -> (Backend, &'static str, &'static str) {
807 if !opts.metadata.sparse && many_small_files(files) {
808 (
809 Backend::Uring,
810 "uring",
811 "auto: many small files (count high, median < 64 KiB)",
812 )
813 } else {
814 (Backend::Portable, "portable", "auto: portable-first")
815 }
816}
817
818#[cfg(windows)]
819fn resolve_auto_backend(
820 _opts: ApplyOptions,
821 _files: &[(&Entry, Action)],
822) -> (Backend, &'static str, &'static str) {
823 (
824 Backend::Portable,
825 "refs/copyfileex",
826 "auto: Windows block-clone / CopyFileExW",
827 )
828}
829
830#[cfg(not(any(all(target_os = "linux", feature = "io-uring"), windows)))]
831fn resolve_auto_backend(
832 _opts: ApplyOptions,
833 _files: &[(&Entry, Action)],
834) -> (Backend, &'static str, &'static str) {
835 (Backend::Portable, "portable", "auto: portable-first")
836}
837
838#[cfg(all(target_os = "linux", feature = "io-uring"))]
840const AUTO_URING_MIN_FILES: usize = 4096;
841#[cfg(all(target_os = "linux", feature = "io-uring"))]
843const AUTO_URING_MEDIAN_MAX: u64 = 64 * 1024;
844
845#[cfg(all(target_os = "linux", feature = "io-uring"))]
847fn many_small_files(files: &[(&Entry, Action)]) -> bool {
848 if files.len() < AUTO_URING_MIN_FILES {
849 return false;
850 }
851 let mut sizes: Vec<u64> = files.iter().map(|(entry, _)| entry.len).collect();
852 let mid = sizes.len() / 2;
853 sizes.select_nth_unstable(mid);
854 sizes[mid] < AUTO_URING_MEDIAN_MAX
855}
856
857fn create_hardlink(root_canon: &Path, entry: &Entry, canonical_rel: &Path) -> Result<()> {
859 let canonical = root_canon.join(canonical_rel);
860 let canonical =
861 std::fs::canonicalize(&canonical).map_err(|error| Error::io(&canonical, error))?;
862 if !canonical.starts_with(root_canon) {
863 return Err(Error::Containment(canonical));
864 }
865 let (target, tmp) = prepare_paths(root_canon, entry)?;
866 std::fs::hard_link(&canonical, &tmp).map_err(|error| Error::io(&tmp, error))?;
867 if let Ok(meta) = std::fs::symlink_metadata(&target) {
868 let result = if meta.is_dir() {
869 std::fs::remove_dir_all(&target)
870 } else {
871 std::fs::remove_file(&target)
872 };
873 if let Err(error) = result {
874 let _ = std::fs::remove_file(&tmp);
875 return Err(Error::io(&target, error));
876 }
877 }
878 if let Err(error) = atomic_replace(&tmp, &target) {
879 let _ = std::fs::remove_file(&tmp);
880 return Err(Error::io(&target, error));
881 }
882 Ok(())
883}
884
885fn create_symlink(
887 src: &Path,
888 root_canon: &Path,
889 entry: &Entry,
890 link_target: &Path,
891 opts: ApplyOptions,
892) -> Result<()> {
893 let link_path = root_canon.join(&entry.rel);
894 let target = contained_target(root_canon, &link_path)?;
895
896 let tmp_link = target.with_file_name(format!(
900 ".ripsync-tmp-{}.{}",
901 target
902 .file_name()
903 .and_then(|n| n.to_str())
904 .unwrap_or("symlink"),
905 std::process::id()
906 ));
907 let _ = std::fs::remove_file(&tmp_link);
909 symlink_impl(link_target, &tmp_link)?;
910 if let Ok(meta) = std::fs::symlink_metadata(&target) {
912 if meta.is_dir() {
913 std::fs::remove_dir_all(&target).map_err(|e| Error::io(&target, e))?;
914 }
915 }
916 std::fs::rename(&tmp_link, &target).map_err(|e| {
917 let _ = std::fs::remove_file(&tmp_link);
918 Error::io(&target, e)
919 })?;
920 set_owner_group(
921 &target,
922 entry.uid,
923 entry.gid,
924 opts.metadata.owner,
925 opts.metadata.group,
926 false,
927 )?;
928 copy_xattrs(
929 &src.join(&entry.rel),
930 &target,
931 opts.metadata.xattrs,
932 opts.metadata.acls,
933 )?;
934 let _ = set_symlink_mtime(&target, entry.mtime);
935 Ok(())
936}
937
938#[cfg(unix)]
939fn symlink_impl(target: &Path, link: &Path) -> Result<()> {
940 std::os::unix::fs::symlink(target, link).map_err(|e| Error::io(link, e))
941}
942
943#[cfg(windows)]
947fn symlink_impl(target: &Path, link: &Path) -> Result<()> {
948 use std::os::windows::fs::{symlink_dir, symlink_file};
949
950 let resolved = link
953 .parent()
954 .map_or_else(|| target.to_path_buf(), |parent| parent.join(target));
955 let is_dir = std::fs::metadata(&resolved)
956 .map(|m| m.is_dir())
957 .unwrap_or(false);
958
959 let result = if is_dir {
960 symlink_dir(target, link)
961 } else {
962 symlink_file(target, link)
963 };
964 match result {
965 Ok(()) => Ok(()),
966 Err(error) if error.raw_os_error() == Some(1314) => {
967 warn_once_symlink_privilege();
968 Ok(())
969 }
970 Err(error) => Err(Error::io(link, error)),
971 }
972}
973
974#[cfg(windows)]
975fn warn_once_symlink_privilege() {
976 use std::sync::atomic::AtomicBool;
977 static WARNED: AtomicBool = AtomicBool::new(false);
978 if !WARNED.swap(true, Ordering::Relaxed) {
979 tracing::warn!(
980 "skipping symlink(s): creating symbolic links on Windows needs \
981 administrator rights or Developer Mode (ERROR_PRIVILEGE_NOT_HELD)"
982 );
983 }
984}
985
986#[cfg(not(any(unix, windows)))]
987fn symlink_impl(_target: &Path, link: &Path) -> Result<()> {
988 Err(Error::io(
989 link,
990 io::Error::new(
991 io::ErrorKind::Unsupported,
992 "symlinks unsupported on this platform",
993 ),
994 ))
995}
996
997fn has_symlink_ancestor(root_canon: &Path, rel: &Path) -> bool {
999 let mut prefix = root_canon.to_path_buf();
1000 let comps: Vec<_> = rel.components().collect();
1001 for comp in &comps[..comps.len().saturating_sub(1)] {
1003 prefix.push(comp);
1004 match std::fs::symlink_metadata(&prefix) {
1005 Ok(m) if m.file_type().is_symlink() => return true,
1006 Ok(_) => {}
1007 Err(_) => return true, }
1009 }
1010 false
1011}
1012
1013fn delete_entry(root_canon: &Path, rel: &Path, is_dir: bool) -> Result<()> {
1019 check_relative(rel)?;
1020 if has_symlink_ancestor(root_canon, rel) {
1024 return Ok(());
1025 }
1026 let path = root_canon.join(rel);
1027 if std::fs::symlink_metadata(&path).is_err() {
1029 return Ok(());
1030 }
1031 let target = contained_target(root_canon, &path)?;
1032 let result = if is_dir {
1033 std::fs::remove_dir(&target).or_else(|_| std::fs::remove_dir_all(&target))
1036 } else {
1037 std::fs::remove_file(&target)
1038 };
1039 match result {
1040 Ok(()) => Ok(()),
1041 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
1042 Err(e) => Err(Error::io(&target, e)),
1043 }
1044}