use super::archive_numbering::physical_archive_names;
use super::recovery::{
any_recoverable_segment, recover_archive_number, select_writable_generation,
};
use super::startup::{RepairedArchive, existing_staging_residue, install_target_generation};
use crate::error::{Error, Result};
use crate::tar_archive::archive::TarArchiveReader;
use crate::tar_archive::file_name::{ArchiveFileName, select_newest_file_generations};
use std::fmt::Write as _;
use std::path::Path;
pub(crate) trait AuthorizeVersionTwoWrite {
fn authorize(&mut self) -> Result<()>;
}
pub(crate) struct VersionTwoAlreadyEstablished;
impl AuthorizeVersionTwoWrite for VersionTwoAlreadyEstablished {
fn authorize(&mut self) -> Result<()> {
Ok(())
}
}
#[cfg(unix)]
pub(crate) fn repair_target_names(directory: &Path) -> Result<Vec<String>> {
let mut by_number: std::collections::BTreeMap<u32, Vec<ArchiveFileName>> =
std::collections::BTreeMap::new();
for parsed in physical_archive_names(directory)? {
by_number
.entry(parsed.archive_number)
.or_default()
.push(parsed);
}
let mut targets = Vec::new();
for mut generations in by_number.into_values() {
generations.sort_by_key(|name| name.file_generation);
let (winner, any_nonempty) = select_writable_generation(directory, &generations);
if winner.is_some() || !any_nonempty {
continue;
}
if any_recoverable_segment(directory, &generations) {
targets.push(
install_target_generation(directory, &generations)
.file_name
.clone(),
);
}
}
Ok(targets)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct IndexlessSurvey {
pub(crate) repairable: usize,
pub(crate) unrepairable: Vec<String>,
}
pub(crate) fn survey_indexless_archive_numbers(directory: &Path) -> Result<IndexlessSurvey> {
let mut by_number: std::collections::BTreeMap<u32, Vec<ArchiveFileName>> =
std::collections::BTreeMap::new();
for parsed in physical_archive_names(directory)? {
by_number
.entry(parsed.archive_number)
.or_default()
.push(parsed);
}
let mut survey = IndexlessSurvey::default();
for mut generations in by_number.into_values() {
generations.sort_by_key(|name| name.file_generation);
let (winner, any_nonempty) = select_writable_generation(directory, &generations);
if winner.is_some() || !any_nonempty {
continue;
}
if any_recoverable_segment(directory, &generations) {
survey.repairable += 1;
} else {
survey.unrepairable.push(
install_target_generation(directory, &generations)
.file_name
.clone(),
);
}
}
Ok(survey)
}
pub(crate) fn unrepairable_archives_refusal(unrepairable: &[String]) -> Error {
Error::InvalidFormat {
details: format!(
"{} active archive(s) have no valid index and no segment any recovery scan can read: \
{}. Repair would refuse them, so this run cannot complete however it is retried; \
move those files aside to proceed, and keep them — they are the only copy of \
whatever they hold",
unrepairable.len(),
unrepairable.join(", ")
),
}
}
pub(crate) fn repair_indexless_archive_numbers(
directory: &Path,
observer: &mut dyn crate::progress::ProgressObserver,
authorize: &mut dyn AuthorizeVersionTwoWrite,
) -> Result<Vec<RepairedArchive>> {
let names = physical_archive_names(directory)?;
select_newest_file_generations(
&names
.iter()
.map(|name| name.file_name.clone())
.collect::<Vec<_>>(),
)?;
let mut by_number: std::collections::BTreeMap<u32, Vec<ArchiveFileName>> =
std::collections::BTreeMap::new();
for parsed in names {
by_number
.entry(parsed.archive_number)
.or_default()
.push(parsed);
}
let total = by_number.len();
crate::progress::observe(
observer,
&crate::progress::Step::new(
"checking archive indexes for repair",
crate::progress::WorkUnit::Archives,
)
.with_total(crate::progress::count(total)),
|observer| {
let mut repaired = Vec::new();
let mut failures: Vec<String> = Vec::new();
for (examined, (number, mut generations)) in by_number.into_iter().enumerate() {
observer.step_advanced(crate::progress::count(examined));
generations.sort_by_key(|name| name.file_generation);
let (winner, any_nonempty) = select_writable_generation(directory, &generations);
if winner.is_some() || !any_nonempty {
continue;
}
let reason = generations
.iter()
.rev()
.find_map(|candidate| {
TarArchiveReader::open(&directory.join(&candidate.file_name))
.ok()
.and_then(|reader| reader.recovery_reason().map(str::to_owned))
})
.unwrap_or_else(|| "the index could not be read".to_owned());
if let Some(residue) = existing_staging_residue(directory, &generations) {
failures.push(format!(
"archive number {number}: {residue} is the residue of an interrupted \
rebuild and may hold the only assembled copy of this archive; \
cleanup's stale-temporaries task decides its fate, so repair will not \
overwrite it — move it aside to retry"
));
continue;
}
match recover_archive_number(directory, &generations, authorize) {
Ok(rebuilt) => repaired.push(RepairedArchive {
file_name: rebuilt.file_name().to_owned(),
reason,
bytes: rebuilt.file_size(),
}),
Err(error) => failures.push(format!("archive number {number}: {error}")),
}
}
observer.step_advanced(crate::progress::count(total));
if failures.is_empty() {
return Ok(repaired);
}
Err(unfinished_repair_refusal(&repaired, &failures))
},
)
}
pub(super) fn unfinished_repair_refusal(
repaired: &[RepairedArchive],
failures: &[String],
) -> Error {
let mut details = format!(
"{} of {} archive index rebuild(s) failed: {}",
failures.len(),
failures.len() + repaired.len(),
failures.join("; ")
);
if !repaired.is_empty() {
let names: Vec<&str> = repaired
.iter()
.map(|archive| archive.file_name.as_str())
.collect();
let _ = write!(
details,
". Already rebuilt and durable, with the originals retained under `.bak` names: {}. \
Those need no second attempt; rerunning repairs only what is left",
names.join(", ")
);
}
Error::InvalidFormat { details }
}