use std::fs;
use std::path::{Path, PathBuf};
use super::durable_record::{CreateExclusive, DurableRecordStore, EntryFence};
use super::persist::{
NoclobberOutcome, sync_parent_dir, write_atomic_noclobber_unfenced, write_atomic_unfenced,
};
use super::refstore::codec::{corrupt_in_file, encode_record};
use super::refstore::{
BranchRefError, BranchRefRecord, REF_EXTENSION, TEMP_PREFIX, TEMP_SUFFIX, decode_record,
ref_file_name,
};
use super::time::Timestamp;
#[derive(Debug)]
pub(super) struct NativeDurableRecordStore {
dir: PathBuf,
}
impl NativeDurableRecordStore {
pub(super) fn open<P: AsRef<Path>>(dir: P) -> Result<Self, BranchRefError> {
let dir = dir.as_ref().to_path_buf();
let created = !dir.exists();
fs::create_dir_all(&dir)?;
if created && let Some(parent) = dir.parent() {
let parent = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
sync_parent_dir(parent)?;
}
Ok(Self { dir })
}
fn ref_path(&self, name: &str) -> PathBuf {
self.dir.join(ref_file_name(name))
}
fn read_record(path: &Path) -> Result<BranchRefRecord, BranchRefError> {
decode_record(&fs::read(path)?).map_err(|error| corrupt_in_file(path, &error))
}
}
impl DurableRecordStore for NativeDurableRecordStore {
type Error = BranchRefError;
fn list_read_at_open(&mut self) -> Result<Vec<BranchRefRecord>, BranchRefError> {
let mut records = Vec::new();
for entry in fs::read_dir(&self.dir)? {
let entry = entry?;
let path = entry.path();
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if file_name.starts_with(TEMP_PREFIX) && file_name.ends_with(TEMP_SUFFIX) {
fs::remove_file(&path)?;
continue;
}
if path.extension().and_then(|extension| extension.to_str()) != Some(REF_EXTENSION) {
continue;
}
let record = Self::read_record(&path)?;
if ref_file_name(&record.name) != file_name {
return Err(BranchRefError::Corrupt(format!(
"ref file {} holds a record for branch {:?}, whose name hashes elsewhere",
path.display(),
record.name,
)));
}
records.push(record);
}
Ok(records)
}
fn create_exclusive(
&mut self,
record: &BranchRefRecord,
) -> Result<CreateExclusive, BranchRefError> {
let path = self.ref_path(&record.name);
match write_atomic_noclobber_unfenced(&path, &encode_record(record))? {
NoclobberOutcome::Installed => Ok(CreateExclusive::Installed),
NoclobberOutcome::TargetExists => {
Ok(CreateExclusive::TargetExists(Self::read_record(&path)?))
}
}
}
fn cas_replace_install(
&mut self,
name: &str,
expected_created: Timestamp,
expected_seq: u64,
replacement: &BranchRefRecord,
) -> Result<BranchRefRecord, BranchRefError> {
let path = self.ref_path(name);
let existing = match Self::read_record(&path) {
Ok(record) => record,
Err(BranchRefError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(BranchRefError::BranchRemoved(name.to_owned()));
}
Err(error) => return Err(error),
};
if existing.created != expected_created {
return Err(BranchRefError::BranchGenerationMismatch {
name: name.to_owned(),
expected_created,
found_created: existing.created,
});
}
if existing.seq != expected_seq {
return Err(BranchRefError::StaleSeq {
name: name.to_owned(),
expected: expected_seq,
found: existing.seq,
});
}
write_atomic_unfenced(&path, &encode_record(replacement))?;
Ok(replacement.clone())
}
fn unlink(&mut self, name: &str) -> Result<bool, BranchRefError> {
match fs::remove_file(self.ref_path(name)) {
Ok(()) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(BranchRefError::Io(error)),
}
}
fn entry_fence(&mut self, _expected: EntryFence<'_>) -> Result<(), BranchRefError> {
sync_parent_dir(&self.dir).map_err(BranchRefError::from)
}
}