ripsync-core 1.2.0

Core sync engine for ripsync: delta engine, parallel walk, plan/apply, and the safety model.
Documentation
use std::path::Path;

use ripsync_core::Filter;
use ripsync_core::apply::{ApplyOptions, MetadataOptions, apply_plan_controlled};
use ripsync_core::plan::{PlanOptions, build_plan_controlled};
use ripsync_core::report::{Event, NullReporter, Reporter, RunPhase};
use ripsync_core::verify::{VerifyMode, verify};
use ripsync_core::{Error, RunControl};
use tempfile::tempdir;

fn write(path: &Path, contents: &[u8]) {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).unwrap();
    }
    std::fs::write(path, contents).unwrap();
}

struct CancelOn {
    control: RunControl,
    phase: Option<RunPhase>,
    file_start: bool,
}

impl Reporter for CancelOn {
    fn event(&self, event: Event) {
        let phase_matches = matches!(event, Event::Phase(phase) if Some(phase) == self.phase);
        if phase_matches || (self.file_start && matches!(event, Event::FileStart { .. })) {
            self.control.cancel();
        }
    }
}

#[test]
fn cancellation_during_planning_is_reported() {
    let temp = tempdir().unwrap();
    let src = temp.path().join("src");
    let dst = temp.path().join("dst");
    write(&src.join("file"), b"content");
    let control = RunControl::default();
    let reporter = CancelOn {
        control: control.clone(),
        phase: Some(RunPhase::Planning),
        file_start: false,
    };
    let excludes = Filter::none();
    let result = build_plan_controlled(
        &src,
        &dst,
        PlanOptions::default(),
        &excludes,
        &control,
        &reporter,
    );
    assert!(matches!(result, Err(Error::Cancelled)));
}

#[test]
fn cancellation_after_an_in_flight_copy_leaves_no_temp_file() {
    let temp = tempdir().unwrap();
    let src = temp.path().join("src");
    let dst = temp.path().join("dst");
    write(&src.join("file"), &[7; 4096]);
    let excludes = Filter::none();
    let plan =
        ripsync_core::plan::build_plan(&src, &dst, PlanOptions::default(), &excludes).unwrap();
    let control = RunControl::default();
    let reporter = CancelOn {
        control: control.clone(),
        phase: None,
        file_start: true,
    };
    let result = apply_plan_controlled(
        &plan,
        &src,
        &dst,
        ApplyOptions {
            threads: 1,
            ..ApplyOptions::default()
        },
        &reporter,
        &control,
    );
    assert!(matches!(result, Err(Error::Cancelled)));
    let names: Vec<_> = std::fs::read_dir(&dst)
        .unwrap()
        .flatten()
        .map(|entry| entry.file_name())
        .collect();
    assert!(
        names
            .iter()
            .all(|name| !name.to_string_lossy().starts_with(".ripsync-tmp-"))
    );
}

#[test]
fn cancellation_before_deletion_keeps_stale_entry() {
    let temp = tempdir().unwrap();
    let src = temp.path().join("src");
    let dst = temp.path().join("dst");
    write(&src.join("keep"), b"same");
    write(&dst.join("keep"), b"same");
    write(&dst.join("stale"), b"stale");
    let excludes = Filter::none();
    let plan = ripsync_core::plan::build_plan(
        &src,
        &dst,
        PlanOptions {
            delete: true,
            ..PlanOptions::default()
        },
        &excludes,
    )
    .unwrap();
    let control = RunControl::default();
    let reporter = CancelOn {
        control: control.clone(),
        phase: Some(RunPhase::Deleting),
        file_start: false,
    };
    let result = apply_plan_controlled(
        &plan,
        &src,
        &dst,
        ApplyOptions {
            delete: true,
            yes: true,
            ..ApplyOptions::default()
        },
        &reporter,
        &control,
    );
    assert!(matches!(result, Err(Error::Cancelled)));
    assert!(dst.join("stale").exists());
}

#[test]
fn verification_reports_content_mismatch_and_can_cancel() {
    let temp = tempdir().unwrap();
    let src = temp.path().join("src");
    let dst = temp.path().join("dst");
    write(&src.join("file"), b"source");
    write(&dst.join("file"), b"destination");
    let excludes = Filter::none();
    let plan =
        ripsync_core::plan::build_plan(&src, &dst, PlanOptions::default(), &excludes).unwrap();
    let summary = verify(
        &plan,
        &src,
        &dst,
        VerifyMode::All,
        MetadataOptions::default(),
        1,
        &RunControl::default(),
        &NullReporter,
    )
    .unwrap();
    assert!(!summary.mismatches.is_empty());

    let control = RunControl::default();
    let reporter = CancelOn {
        control: control.clone(),
        phase: Some(RunPhase::Verifying),
        file_start: false,
    };
    let result = verify(
        &plan,
        &src,
        &dst,
        VerifyMode::All,
        MetadataOptions::default(),
        1,
        &control,
        &reporter,
    );
    assert!(matches!(result, Err(Error::Cancelled)));
}