use std::path::{Path, PathBuf};
use std::collections::{HashMap, HashSet};
use rayon::prelude::*;
use crate::filter::Filter;
use crate::index::{Manifest, RIPSYNC_DIR};
use crate::report::{Event, NullReporter, Reporter, RunPhase};
use crate::walk::{Entry, EntryKind, walk_controlled};
use crate::{Error, Result, RunControl};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Copy,
Update,
Skip,
}
#[derive(Debug, Clone)]
pub struct PlannedAction {
pub entry: Entry,
pub action: Action,
}
#[derive(Debug, Clone)]
pub struct Deletion {
pub rel: PathBuf,
pub is_dir: bool,
}
#[derive(Debug, Clone, Copy, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct PlanOptions {
pub checksum: bool,
pub delete: bool,
pub threads: usize,
pub index: bool,
pub hard_links: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SyncPlan {
pub actions: Vec<PlannedAction>,
pub deletions: Vec<Deletion>,
}
impl SyncPlan {
#[must_use]
pub fn count(&self, action: Action) -> usize {
self.actions.iter().filter(|a| a.action == action).count()
}
#[must_use]
pub fn bytes_to_transfer(&self) -> u64 {
self.actions
.iter()
.filter(|a| a.action != Action::Skip && a.entry.is_file())
.map(|a| a.entry.len)
.sum()
}
}
fn file_hash(path: &Path) -> Option<[u8; 32]> {
const MMAP_HASH_THRESHOLD: u64 = 16 * 1024 * 1024;
let len = std::fs::metadata(path).ok()?.len();
let mut hasher = blake3::Hasher::new();
if len >= MMAP_HASH_THRESHOLD {
hasher.update_mmap_rayon(path).ok()?;
} else {
let bytes = std::fs::read(path).ok()?;
hasher.update(&bytes);
}
Some(*hasher.finalize().as_bytes())
}
fn files_differ(
src_root: &Path,
dst_root: &Path,
entry: &Entry,
dst: &Entry,
checksum: bool,
) -> bool {
let metadata_differs = entry.mode & 0o7777 != dst.mode & 0o7777
|| entry.mtime.unix_seconds() != dst.mtime.unix_seconds();
if checksum {
let sp = src_root.join(&entry.rel);
let dp = dst_root.join(&dst.rel);
match (file_hash(&sp), file_hash(&dp)) {
(Some(a), Some(b)) => metadata_differs || a != b,
_ => true,
}
} else {
entry.len != dst.len || metadata_differs
}
}
pub fn build_plan(
src: &Path,
dst: &Path,
opts: PlanOptions,
filter: &Filter,
) -> Result<SyncPlan> {
build_plan_controlled(
src,
dst,
opts,
filter,
&RunControl::default(),
&NullReporter,
)
}
pub fn build_plan_controlled<R: Reporter>(
src: &Path,
dst: &Path,
opts: PlanOptions,
filter: &Filter,
control: &RunControl,
reporter: &R,
) -> Result<SyncPlan> {
reporter.event(Event::Phase(RunPhase::Planning));
control.checkpoint()?;
let mut src_entries = walk_controlled(src, opts.threads, filter, control)?;
control.checkpoint()?;
reporter.event(Event::PlanningProgress {
entries: src_entries.len(),
});
src_entries.retain(|e| !e.rel.starts_with(RIPSYNC_DIR));
if opts.delete && src_entries.is_empty() {
return Err(Error::EmptySource(src.to_path_buf()));
}
if opts.index && !opts.delete {
if let Some(manifest) = Manifest::load(dst) {
return Ok(plan_from_manifest(src, dst, &src_entries, &manifest, opts));
}
}
let mut dst_entries = if dst.exists() {
walk_controlled(dst, opts.threads, filter, control)?
} else {
Vec::new()
};
control.checkpoint()?;
reporter.event(Event::PlanningProgress {
entries: src_entries.len() + dst_entries.len(),
});
dst_entries.retain(|e| !e.rel.starts_with(RIPSYNC_DIR));
let dst_map: HashMap<&Path, &Entry> =
dst_entries.iter().map(|e| (e.rel.as_path(), e)).collect();
let mut actions: Vec<PlannedAction> = src_entries
.par_iter()
.map(|entry| {
let action = match dst_map.get(entry.rel.as_path()) {
None => Action::Copy,
Some(dst_entry) => classify_existing(src, dst, entry, dst_entry, opts.checksum),
};
PlannedAction {
entry: entry.clone(),
action,
}
})
.collect();
if opts.hard_links {
enforce_hardlink_actions(&mut actions, |rel| {
dst_map.get(rel).map(|entry| (entry.dev, entry.ino))
});
}
let mut deletions = Vec::new();
if opts.delete {
let src_set: HashSet<&Path> = src_entries.iter().map(|e| e.rel.as_path()).collect();
for d in &dst_entries {
if !src_set.contains(d.rel.as_path()) {
deletions.push(Deletion {
rel: d.rel.clone(),
is_dir: d.is_dir(),
});
}
}
deletions.sort_by(|a, b| b.rel.cmp(&a.rel));
}
Ok(SyncPlan { actions, deletions })
}
fn plan_from_manifest(
src: &Path,
dst: &Path,
src_entries: &[Entry],
manifest: &Manifest,
opts: PlanOptions,
) -> SyncPlan {
let mut actions: Vec<PlannedAction> = src_entries
.par_iter()
.map(|entry| PlannedAction {
entry: entry.clone(),
action: manifest.classify(entry, opts.checksum, src, dst),
})
.collect();
if opts.hard_links {
enforce_hardlink_actions(&mut actions, |rel| {
crate::meta::meta_min(&dst.join(rel))
.ok()
.map(|entry| (entry.dev, entry.ino))
});
}
let mut deletions = Vec::new();
if opts.delete {
let src_set: HashSet<&Path> = src_entries.iter().map(|e| e.rel.as_path()).collect();
for (rel, rec) in &manifest.entries {
if !src_set.contains(rel.as_path()) {
deletions.push(Deletion {
rel: rel.clone(),
is_dir: rec.kind == crate::index::Kind::Dir,
});
}
}
deletions.sort_by(|a, b| b.rel.cmp(&a.rel));
}
SyncPlan { actions, deletions }
}
fn enforce_hardlink_actions(
actions: &mut [PlannedAction],
destination_identity: impl Fn(&Path) -> Option<(u64, u64)>,
) {
let mut first: HashMap<(u64, u64), Option<(u64, u64)>> = HashMap::new();
for planned in actions {
if !planned.entry.is_file() {
continue;
}
let source_id = (planned.entry.dev, planned.entry.ino);
if let Some(canonical_destination) = first.get(&source_id) {
if destination_identity(&planned.entry.rel) != *canonical_destination {
planned.action = Action::Update;
}
} else {
first.insert(source_id, destination_identity(&planned.entry.rel));
}
}
}
fn classify_existing(
src: &Path,
dst: &Path,
entry: &Entry,
dst_entry: &Entry,
checksum: bool,
) -> Action {
match (&entry.kind, &dst_entry.kind) {
(EntryKind::Dir, EntryKind::Dir) => {
if entry.mode & 0o7777 == dst_entry.mode & 0o7777
&& entry.mtime.unix_seconds() == dst_entry.mtime.unix_seconds()
{
Action::Skip
} else {
Action::Update
}
}
(EntryKind::Symlink(a), EntryKind::Symlink(b)) => {
if a == b && entry.mtime.unix_seconds() == dst_entry.mtime.unix_seconds() {
Action::Skip
} else {
Action::Update
}
}
(EntryKind::File, EntryKind::File) => {
if files_differ(src, dst, entry, dst_entry, checksum) {
Action::Update
} else {
Action::Skip
}
}
_ => Action::Update,
}
}