use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::branch::{BranchRefRecord, SnapshotEntry};
use super::error::VacuumError;
use super::manifest::{ManifestSourceKind, MetadataManifest};
use super::metadata::{read_refs_dir, read_snapshot_registry};
use super::report::{
ManifestEntryReport, ManifestReport, MetadataOrigin, MetadataSourceKind, MetadataSourceReport,
SweepBlocker,
};
use super::{CANONICAL_REFS_DIR, CANONICAL_SNAPSHOT_REGISTRY, VacuumOptions};
pub struct MetadataPass {
pub manifest_report: Option<ManifestReport>,
pub sources: Vec<MetadataSourceReport>,
pub refs_scans: Vec<RefsSource>,
pub registries: Vec<RegistrySource>,
pub supplied_missing: Vec<PathBuf>,
pub blockers: Vec<SweepBlocker>,
pub listed_paths: Vec<PathBuf>,
}
pub struct RefsSource {
pub records: Vec<BranchRefRecord>,
}
pub struct RegistrySource {
pub path: PathBuf,
pub entries: Vec<SnapshotEntry>,
pub declared_shards: Option<Vec<usize>>,
}
pub fn consult_metadata(
data_dir: &Path,
options: &VacuumOptions,
manifest: Option<&MetadataManifest>,
) -> Result<MetadataPass, VacuumError> {
let mut pass = MetadataPass {
manifest_report: None,
sources: Vec::new(),
refs_scans: Vec::new(),
registries: Vec::new(),
supplied_missing: Vec::new(),
blockers: Vec::new(),
listed_paths: Vec::new(),
};
let mut consulted: HashSet<PathBuf> = HashSet::new();
if let Some(manifest) = manifest {
consult_manifest_entries(data_dir, manifest, &mut pass, &mut consulted)?;
} else if !options.attest_metadata_complete {
pass.blockers.push(SweepBlocker::MetadataUnattested);
}
consult_canonical(data_dir, manifest.is_some(), &mut pass, &mut consulted)?;
consult_supplied(options, &mut pass, &mut consulted)?;
Ok(pass)
}
fn consult_manifest_entries(
data_dir: &Path,
manifest: &MetadataManifest,
pass: &mut MetadataPass,
consulted: &mut HashSet<PathBuf>,
) -> Result<(), VacuumError> {
let mut entry_reports = Vec::new();
for entry in &manifest.entries {
let path = MetadataManifest::resolved_path(data_dir, entry);
pass.listed_paths.push(path.clone());
if !entry.state.is_settled() {
pass.blockers.push(SweepBlocker::ManifestEntryUnsettled {
path: path.clone(),
state: entry.state.display_name(),
});
}
let mut readable = false;
match probe_path(&path) {
PathPresence::Present => {
readable = attempt_consult(
entry.kind,
&path,
MetadataOrigin::Manifest,
entry.shards.clone(),
pass,
)?;
consulted.insert(path.clone());
}
PathPresence::Absent => {
pass.blockers
.push(SweepBlocker::ManifestSourceMissing { path: path.clone() });
}
PathPresence::Inaccessible(detail) => {
pass.blockers.push(SweepBlocker::SourceUnreadable {
path: path.clone(),
detail,
});
}
}
entry_reports.push(ManifestEntryReport {
path,
kind: map_kind(entry.kind),
state: entry.state.display_name(),
source_readable: readable,
});
}
pass.manifest_report = Some(ManifestReport {
path: data_dir.join(super::manifest::MANIFEST_FILE),
generation: manifest.generation,
entries: entry_reports,
});
Ok(())
}
fn consult_canonical(
data_dir: &Path,
manifest_present: bool,
pass: &mut MetadataPass,
consulted: &mut HashSet<PathBuf>,
) -> Result<(), VacuumError> {
let canonical = [
(
data_dir.join(CANONICAL_REFS_DIR),
ManifestSourceKind::RefsDir,
),
(
data_dir.join(CANONICAL_SNAPSHOT_REGISTRY),
ManifestSourceKind::SnapshotRegistry,
),
];
for (path, kind) in canonical {
if consulted.contains(&path) {
continue;
}
match probe_path(&path) {
PathPresence::Absent => continue,
PathPresence::Inaccessible(detail) => {
pass.blockers
.push(SweepBlocker::SourceUnreadable { path, detail });
continue;
}
PathPresence::Present => {}
}
if manifest_present {
pass.blockers.push(SweepBlocker::StaleManifest {
unlisted: path.clone(),
});
}
attempt_consult(kind, &path, MetadataOrigin::Canonical, None, pass)?;
consulted.insert(path);
}
Ok(())
}
fn consult_supplied(
options: &VacuumOptions,
pass: &mut MetadataPass,
consulted: &mut HashSet<PathBuf>,
) -> Result<(), VacuumError> {
let supplied = options
.refs_dirs
.iter()
.map(|path| (path.clone(), ManifestSourceKind::RefsDir))
.chain(
options
.snapshot_files
.iter()
.map(|path| (path.clone(), ManifestSourceKind::SnapshotRegistry)),
);
for (path, kind) in supplied {
if consulted.contains(&path) {
continue;
}
match probe_path(&path) {
PathPresence::Absent => {
pass.supplied_missing.push(path.clone());
pass.blockers
.push(SweepBlocker::SuppliedPathMissing { path });
continue;
}
PathPresence::Inaccessible(detail) => {
pass.blockers
.push(SweepBlocker::SourceUnreadable { path, detail });
continue;
}
PathPresence::Present => {}
}
attempt_consult(kind, &path, MetadataOrigin::Supplied, None, pass)?;
consulted.insert(path);
}
Ok(())
}
enum PathPresence {
Present,
Absent,
Inaccessible(String),
}
fn probe_path(path: &Path) -> PathPresence {
match std::fs::symlink_metadata(path) {
Ok(_metadata) => PathPresence::Present,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => PathPresence::Absent,
Err(error) => PathPresence::Inaccessible(error.to_string()),
}
}
fn attempt_consult(
kind: ManifestSourceKind,
path: &Path,
origin: MetadataOrigin,
declared_shards: Option<Vec<usize>>,
pass: &mut MetadataPass,
) -> Result<bool, VacuumError> {
match consult_source(kind, path, origin, declared_shards, pass) {
Ok(()) => Ok(true),
Err(VacuumError::Io { error, .. }) => {
pass.blockers.push(SweepBlocker::SourceUnreadable {
path: path.to_path_buf(),
detail: error.to_string(),
});
Ok(false)
}
Err(other) => Err(other),
}
}
fn consult_source(
kind: ManifestSourceKind,
path: &Path,
origin: MetadataOrigin,
declared_shards: Option<Vec<usize>>,
pass: &mut MetadataPass,
) -> Result<(), VacuumError> {
match kind {
ManifestSourceKind::RefsDir => {
let scan = read_refs_dir(path)?;
pass.sources.push(MetadataSourceReport {
path: path.to_path_buf(),
kind: MetadataSourceKind::RefsDir,
origin,
records: scan.records.len(),
temp_debris_files: scan.temp_debris_files,
foreign_entries: scan.foreign_entries,
});
pass.refs_scans.push(RefsSource {
records: scan.records,
});
}
ManifestSourceKind::SnapshotRegistry => {
let entries = read_snapshot_registry(path)?;
pass.sources.push(MetadataSourceReport {
path: path.to_path_buf(),
kind: MetadataSourceKind::SnapshotRegistry,
origin,
records: entries.len(),
temp_debris_files: 0,
foreign_entries: 0,
});
pass.registries.push(RegistrySource {
path: path.to_path_buf(),
entries,
declared_shards,
});
}
}
Ok(())
}
const fn map_kind(kind: ManifestSourceKind) -> MetadataSourceKind {
match kind {
ManifestSourceKind::RefsDir => MetadataSourceKind::RefsDir,
ManifestSourceKind::SnapshotRegistry => MetadataSourceKind::SnapshotRegistry,
}
}