use std::{collections::BTreeSet, fmt, path::PathBuf};
use anyhow::Result;
use clap::Args;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use io_pimdir::{PimdirDangling, PimdirMinted, PimdirRefcountDrift};
use crate::cli::{StoreFlags, bytes, report};
const SHOWN: usize = 20;
#[derive(Debug, Args)]
pub struct CheckCommand {
#[arg(long)]
pub fix: bool,
}
impl CheckCommand {
pub fn execute(self, printer: &mut impl Printer, store: &StoreFlags) -> Result<()> {
let read = store.read()?;
let indexed = read.indexed_hashes().map_err(report)?;
let on_disk = read.blobs().files()?;
let mut orphans = Vec::new();
let mut orphan_bytes = 0;
for blob in &on_disk {
if !indexed.contains(&blob.hash) {
orphan_bytes += blob.size;
orphans.push(blob.clone());
}
}
let names: BTreeSet<&String> = on_disk.iter().map(|blob| &blob.hash).collect();
let missing: Vec<String> = indexed
.iter()
.filter(|hash| !names.contains(hash))
.cloned()
.collect();
let drift = read.refcount_drift().map_err(report)?;
let dangling = read.dangling().map_err(report)?;
let minted = read.minted_keys().map_err(report)?;
let mut repaired = 0;
let mut cleared = 0;
if self.fix && (!drift.is_empty() || !dangling.is_empty()) {
drop(read);
let owner = store.owner()?;
repaired = owner.recompute_refcounts().map_err(report)?;
cleared = owner.clear_dangling_bindings().map_err(report)?;
}
printer.out(CheckOutput {
orphans: orphans
.into_iter()
.map(|blob| OrphanBlob {
hash: blob.hash,
size: blob.size,
path: blob.path,
})
.collect(),
orphan_bytes,
missing,
drift,
dangling,
minted,
repaired,
cleared,
})
}
}
#[derive(Debug, Serialize)]
pub struct OrphanBlob {
pub hash: String,
pub size: u64,
pub path: PathBuf,
}
#[derive(Debug, Serialize)]
pub struct CheckOutput {
pub orphans: Vec<OrphanBlob>,
pub orphan_bytes: u64,
pub missing: Vec<String>,
pub drift: Vec<PimdirRefcountDrift>,
pub dangling: Vec<PimdirDangling>,
pub minted: Vec<PimdirMinted>,
pub repaired: usize,
pub cleared: usize,
}
impl CheckOutput {
fn problems(&self) -> usize {
self.orphans.len() + self.missing.len() + self.drift.len() + self.dangling.len()
}
}
impl fmt::Display for CheckOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.problems() == 0 {
writeln!(
f,
"This store is consistent: no orphan blob, no refcount drift, no dangling row"
)?;
}
if !self.orphans.is_empty() {
writeln!(
f,
"{} orphan blob file(s), {} not referenced by any object row:",
self.orphans.len(),
bytes(self.orphan_bytes)
)?;
for orphan in self.orphans.iter().take(SHOWN) {
writeln!(f, " - {} ({})", orphan.hash, bytes(orphan.size))?;
}
more(f, self.orphans.len())?;
writeln!(f, " Reclaim them with `pimdir gc`")?;
}
if !self.missing.is_empty() {
writeln!(
f,
"{} object row(s) whose body is missing from the blob store:",
self.missing.len()
)?;
for hash in self.missing.iter().take(SHOWN) {
writeln!(f, " - {hash}")?;
}
more(f, self.missing.len())?;
}
if !self.drift.is_empty() {
writeln!(f, "{} object(s) with a drifted refcount:", self.drift.len())?;
for drift in self.drift.iter().take(SHOWN) {
writeln!(
f,
" - {}: stored {}, references {}",
drift.hash, drift.stored, drift.expected
)?;
}
more(f, self.drift.len())?;
}
if !self.dangling.is_empty() {
writeln!(f, "{} dangling row(s):", self.dangling.len())?;
for dangling in self.dangling.iter().take(SHOWN) {
writeln!(
f,
" - {} {} points at a missing {}",
dangling.kind, dangling.row, dangling.target
)?;
}
more(f, self.dangling.len())?;
}
if !self.minted.is_empty() {
writeln!(
f,
"Minted keys, the second copies of an identity a source holds twice:"
)?;
for minted in self.minted.iter().take(SHOWN) {
writeln!(f, " - {}: {} item(s)", minted.collection, minted.items)?;
}
more(f, self.minted.len())?;
}
if self.repaired > 0 || self.cleared > 0 {
writeln!(
f,
"Repaired {} refcount(s) and cleared {} dangling binding(s)",
self.repaired, self.cleared
)?;
}
Ok(())
}
}
fn more(f: &mut fmt::Formatter<'_>, total: usize) -> fmt::Result {
if total > SHOWN {
writeln!(
f,
" … and {} more (use --json for the full list)",
total - SHOWN
)?;
}
Ok(())
}