use std::cmp::Reverse;
use crate::config::{CompareMode, SyncConfig};
use crate::error::{FastSyncError, Result};
use crate::hash::same_blake3;
use crate::plan::{CopyReason, PlanOperation, SyncPlan};
use crate::scan::{EntryKind, FileEntry, Snapshot};
pub fn build_plan(config: &SyncConfig, source: &Snapshot, target: &Snapshot) -> Result<SyncPlan> {
let mut plan = SyncPlan::default();
for source_entry in source.entries.values() {
match source_entry.kind {
EntryKind::Directory => {
if target.get(&source_entry.relative_path).is_none() {
plan.push(PlanOperation::CreateDirectory {
relative_path: source_entry.relative_path.clone(),
});
} else if let Some(target_entry) = target.get(&source_entry.relative_path) {
ensure_same_kind(source_entry, target_entry)?;
}
}
EntryKind::File => plan_file(config, source_entry, target, &mut plan)?,
EntryKind::Symlink => {}
}
}
if config.delete {
let mut obsolete: Vec<_> = target
.entries
.values()
.filter(|entry| source.get(&entry.relative_path).is_none())
.collect();
obsolete.sort_by_key(|entry| Reverse(entry.relative_path.components().count()));
for entry in obsolete {
match entry.kind {
EntryKind::File => plan.push(PlanOperation::DeleteFile {
relative_path: entry.relative_path.clone(),
}),
EntryKind::Directory => plan.push(PlanOperation::DeleteDirectory {
relative_path: entry.relative_path.clone(),
}),
EntryKind::Symlink => plan.push(PlanOperation::DeleteSymlink {
relative_path: entry.relative_path.clone(),
}),
}
}
}
Ok(plan)
}
fn plan_file(
config: &SyncConfig,
source_entry: &FileEntry,
target: &Snapshot,
plan: &mut SyncPlan,
) -> Result<()> {
let Some(target_entry) = target.get(&source_entry.relative_path) else {
plan.push(PlanOperation::CopyFile {
relative_path: source_entry.relative_path.clone(),
bytes: source_entry.len,
reason: CopyReason::Missing,
});
return Ok(());
};
ensure_same_kind(source_entry, target_entry)?;
if let Some(reason) = content_change_reason(config, source_entry, target_entry, plan)? {
plan.push(PlanOperation::CopyFile {
relative_path: source_entry.relative_path.clone(),
bytes: source_entry.len,
reason,
});
} else if config.syncs_file_metadata()
&& sync_metadata_differs(config, source_entry, target_entry)
{
plan.push(PlanOperation::SetMetadata {
relative_path: source_entry.relative_path.clone(),
});
}
Ok(())
}
fn content_change_reason(
config: &SyncConfig,
source_entry: &FileEntry,
target_entry: &FileEntry,
plan: &mut SyncPlan,
) -> Result<Option<CopyReason>> {
match config.compare_mode {
CompareMode::Fast => {
if !content_metadata_differs(source_entry, target_entry) {
Ok(None)
} else if source_entry.len != target_entry.len {
Ok(Some(CopyReason::MetadataChanged))
} else if same_content_by_blake3(source_entry, target_entry, plan)? {
Ok(None)
} else {
Ok(Some(CopyReason::ContentChanged))
}
}
CompareMode::Strict => {
if source_entry.len != target_entry.len {
Ok(Some(CopyReason::MetadataChanged))
} else if same_content_by_blake3(source_entry, target_entry, plan)? {
Ok(None)
} else {
Ok(Some(CopyReason::ContentChanged))
}
}
}
}
fn same_content_by_blake3(
source_entry: &FileEntry,
target_entry: &FileEntry,
plan: &mut SyncPlan,
) -> Result<bool> {
plan.record_blake3_comparison();
same_blake3(&source_entry.absolute_path, &target_entry.absolute_path)
}
fn ensure_same_kind(source: &FileEntry, target: &FileEntry) -> Result<()> {
if source.kind == target.kind {
return Ok(());
}
Err(FastSyncError::PathTypeConflict {
relative_path: source.relative_path.clone(),
source_kind: source.kind.as_str(),
target_kind: target.kind.as_str(),
})
}
fn content_metadata_differs(source: &FileEntry, target: &FileEntry) -> bool {
source.len != target.len
|| source.modified != target.modified
|| permission_metadata_differs(source, target)
}
fn sync_metadata_differs(config: &SyncConfig, source: &FileEntry, target: &FileEntry) -> bool {
let time_differs = config.preserve_times.enabled() && source.modified != target.modified;
let permission_differs =
config.preserve_permissions.enabled() && permission_metadata_differs(source, target);
time_differs || permission_differs
}
fn permission_metadata_differs(source: &FileEntry, target: &FileEntry) -> bool {
source.readonly != target.readonly || platform_permissions_differ(source, target)
}
fn platform_permissions_differ(source: &FileEntry, target: &FileEntry) -> bool {
#[cfg(unix)]
{
source.mode != target.mode
}
#[cfg(not(unix))]
{
let _ = (source, target);
false
}
}