use std::path::{Path, PathBuf};
use crate::shard::router::{SHARD_STORE_DIR, SHARD_WAL_FILE};
use crate::tree::Hash;
use crate::wal::WalRecovery;
use super::super::error::VacuumError;
use super::super::inventory::{StoreInventory, enumerate_store, lstat_kind};
use super::super::report::{EntryKind, MalformedEntry, ShardPresence};
pub struct ShardScan {
pub shard_id: usize,
pub presence: ShardPresence,
pub committed_root: Option<Hash>,
pub store_dir: PathBuf,
pub inventory: StoreInventory,
pub shard_malformed: Vec<MalformedEntry>,
}
pub fn scan_shard(data_dir: &Path, shard_id: usize) -> Result<ShardScan, VacuumError> {
let shard_dir = data_dir.join(format!("shard-{shard_id}"));
let store_dir = shard_dir.join(SHARD_STORE_DIR);
match lstat_kind(&shard_dir)? {
None => {
return Ok(ShardScan {
shard_id,
presence: ShardPresence::NeverMaterialised,
committed_root: None,
store_dir,
inventory: StoreInventory::default(),
shard_malformed: Vec::new(),
});
}
Some(EntryKind::Directory) => {}
Some(kind) => {
return Err(VacuumError::UnexpectedLayout {
store: store_dir,
path: shard_dir,
reason: format!(
"shard entry is a {kind:?}, not a real directory; only a wholly absent \
shard-{{id}} is lazy, and symlinks are never followed at model boundaries"
),
});
}
}
let wal_path = shard_dir.join(SHARD_WAL_FILE);
let store_kind = lstat_kind(&store_dir)?;
match lstat_kind(&wal_path)? {
None => {
return Err(VacuumError::StoreWithoutWal {
shard_id,
shard_dir,
store_present: store_kind.is_some(),
});
}
Some(EntryKind::File) => {}
Some(kind) => {
return Err(VacuumError::UnexpectedLayout {
store: store_dir,
path: wal_path,
reason: format!("shard.wal is a {kind:?}, not a regular file"),
});
}
}
let recovered = WalRecovery::open(&wal_path)
.and_then(|mut recovery| recovery.recover_unverified())
.map_err(|error| VacuumError::CorruptWal {
shard_id,
detail: error.to_string(),
})?;
if recovered.stopped_at_corruption() {
return Err(VacuumError::CorruptWal {
shard_id,
detail: "replay stopped at a checksum-corrupted frame".to_owned(),
});
}
match store_kind {
Some(EntryKind::Directory) => {}
None => {
return Err(VacuumError::UnexpectedLayout {
store: store_dir,
path: shard_dir,
reason: "shard.wal exists but the store directory is absent; boot creates the \
store before the WAL, so this shape is damage"
.to_owned(),
});
}
Some(kind) => {
return Err(VacuumError::UnexpectedLayout {
store: store_dir.clone(),
path: store_dir,
reason: format!("store is a {kind:?}, not a real directory"),
});
}
}
let shard_malformed = scan_shard_dir(&shard_dir)?;
let inventory = enumerate_store(&store_dir)?;
Ok(ShardScan {
shard_id,
presence: ShardPresence::Materialised,
committed_root: recovered.committed_root(),
store_dir,
inventory,
shard_malformed,
})
}
fn scan_shard_dir(shard_dir: &Path) -> Result<Vec<MalformedEntry>, VacuumError> {
let mut malformed = Vec::new();
let entries = std::fs::read_dir(shard_dir).map_err(|error| VacuumError::Io {
path: shard_dir.to_path_buf(),
error,
})?;
for entry in entries {
let entry = entry.map_err(|error| VacuumError::Io {
path: shard_dir.to_path_buf(),
error,
})?;
let path = entry.path();
let expected = path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name == SHARD_STORE_DIR || name == SHARD_WAL_FILE);
if !expected {
malformed.push(MalformedEntry {
path,
reason: "unexpected entry in shard directory".to_owned(),
});
}
}
Ok(malformed)
}