From 2c3eb11317972ba5fa87e717cf14e8eb1dfc83c7 Mon Sep 17 00:00:00 2001 From: Magrathean UK Date: Sat, 1 Aug 2026 12:05:22 +0100 Subject: [PATCH] fix: check cold data for hot and cold repositories (#1811) --- src/commands/check.rs | 42 +++++++++++++++++++++++++++------ tests/check_hot_cold.rs | 51 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 tests/check_hot_cold.rs diff --git a/src/commands/check.rs b/src/commands/check.rs index 4c7460a45..6077ce8c2 100644 --- a/src/commands/check.rs +++ b/src/commands/check.rs @@ -8,7 +8,7 @@ use crate::{ use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; -use rustic_core::{CheckOptions, repofile::SnapshotFile}; +use rustic_core::{CheckOptions, Credentials, repofile::SnapshotFile}; /// `check` subcommand #[derive(clap::Parser, Command, Debug)] @@ -26,11 +26,35 @@ pub(crate) struct CheckCmd { impl Runnable for CheckCmd { fn run(&self) { - if let Err(err) = RUSTIC_APP - .config() - .repository - .run_open(|repo| self.inner_run(repo)) - { + let config = RUSTIC_APP.config(); + let repository = &config.repository; + + // A hot repository intentionally contains only metadata and tree packs; + // data packs live in the cold repository. First preserve the usual + // hot/cold consistency checks without reading data. Then create an + // isolated cold-only repository for the pack-data check. Reuse the + // validated master key so an interactive user is not prompted twice. + let result = if self.opts.read_data && repository.be.repo_hot.is_some() { + let mut cold_repository = repository.clone(); + cold_repository.be.repo_hot = None; + repository.run(|repo| { + let repo = repo.open(&repository.credential_opts)?; + let credentials = Credentials::Masterkey(repo.key()); + let mut metadata_opts = self.opts; + metadata_opts.read_data = false; + self.inner_run_with_options(repo, metadata_opts)?; + + let cold_repo = cold_repository + .repository(config.global.progress_options)? + .0 + .open(&credentials)?; + self.inner_run(cold_repo) + }) + } else { + repository.run_open(|repo| self.inner_run(repo)) + }; + + if let Err(err) = result { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -39,9 +63,13 @@ impl Runnable for CheckCmd { impl CheckCmd { fn inner_run(&self, repo: OpenRepo) -> Result<()> { + self.inner_run_with_options(repo, self.opts) + } + + fn inner_run_with_options(&self, repo: OpenRepo, opts: CheckOptions) -> Result<()> { let snaps: Vec = get_global_grouped_snapshots(&repo, &self.ids)?.into(); let trees = snaps.into_iter().map(|snap| snap.tree).collect(); - repo.check_with_trees(self.opts, trees)?.is_ok()?; + repo.check_with_trees(opts, trees)?.is_ok()?; Ok(()) } } diff --git a/tests/check_hot_cold.rs b/tests/check_hot_cold.rs new file mode 100644 index 000000000..495925c84 --- /dev/null +++ b/tests/check_hot_cold.rs @@ -0,0 +1,51 @@ +use std::path::Path; + +use assert_cmd::Command; +use rustic_testing::TestResult; +use tempfile::tempdir; + +fn rustic_runner(profile: &Path, cold_repo: &Path, hot_repo: &Path) -> TestResult { + let mut runner = Command::new(env!("CARGO_BIN_EXE_rustic")); + runner + .arg("--use-profile") + .arg(profile) + .arg("--repository") + .arg(cold_repo) + .arg("--repo-hot") + .arg(hot_repo) + .args(["--password", "test", "--no-progress", "--no-cache"]); + Ok(runner) +} + +#[test] +fn check_read_data_uses_cold_data_packs_for_hot_cold_repositories() -> TestResult<()> { + let temp_dir = tempdir()?; + let cold_repo = temp_dir.path().join("cold-repository"); + let hot_repo = temp_dir.path().join("hot-repository"); + let source = temp_dir.path().join("source"); + let profile = temp_dir.path().join("test.toml"); + std::fs::write(&profile, "")?; + std::fs::create_dir(&source)?; + std::fs::write(source.join("payload.bin"), vec![42_u8; 256 * 1024])?; + + rustic_runner(&profile, &cold_repo, &hot_repo)? + .arg("init") + .assert() + .success(); + + rustic_runner(&profile, &cold_repo, &hot_repo)? + .arg("backup") + .arg(&source) + .assert() + .success(); + + // Data packs are intentionally stored only in the cold repository. A + // successful read-data check proves it reads them there rather than trying + // the hot metadata repository. + rustic_runner(&profile, &cold_repo, &hot_repo)? + .args(["check", "--read-data"]) + .assert() + .success(); + + Ok(()) +}