use crate::system::SystemPath;
use std::path::Path;
use std::str::FromStr;
use uuid::Uuid;
use crate::error::FSError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VaultId(Uuid);
impl VaultId {
pub fn new_random() -> Self {
Self(Uuid::new_v4())
}
}
impl std::fmt::Display for VaultId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
fn vault_id_path(workspace_path: &SystemPath) -> std::path::PathBuf {
workspace_path.as_path().join(".kimun").join("vault-id")
}
pub async fn read_or_create_vault_id(workspace_path: &SystemPath) -> Result<VaultId, FSError> {
let path = vault_id_path(workspace_path);
if let Ok(body) = tokio::fs::read_to_string(&path).await {
if let Ok(uuid) = Uuid::from_str(body.trim()) {
return Ok(VaultId(uuid));
}
}
tokio::task::spawn_blocking(move || settle_vault_id(&path))
.await
.map_err(|e| FSError::ReadFileError(std::io::Error::other(e)))?
}
fn settle_vault_id(path: &Path) -> Result<VaultId, FSError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let lock_file = std::fs::OpenOptions::new()
.create(true)
.truncate(false) .write(true)
.open(path.with_extension("lock"))?;
lock_file.lock()?;
match std::fs::read_to_string(path) {
Ok(body) => {
let trimmed = body.trim();
if let Ok(uuid) = Uuid::from_str(trimmed) {
return Ok(VaultId(uuid));
}
if trimmed.is_empty() {
std::fs::remove_file(path)?;
} else {
let backup = path.with_extension("corrupt");
let _ = std::fs::remove_file(&backup);
std::fs::rename(path, &backup)?;
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(FSError::ReadFileError(e)),
}
let id = VaultId::new_random();
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, id.to_string())?;
std::fs::rename(&tmp, path)?;
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn creates_and_persists_on_first_read() {
let dir = tempfile::TempDir::new().unwrap();
let id = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
assert!(dir.path().join(".kimun").join("vault-id").exists());
let again = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
assert_eq!(id, again);
}
#[tokio::test]
async fn corrupt_file_self_heals() {
let dir = tempfile::TempDir::new().unwrap();
let kimun = dir.path().join(".kimun");
tokio::fs::create_dir_all(&kimun).await.unwrap();
tokio::fs::write(kimun.join("vault-id"), "not-a-uuid")
.await
.unwrap();
let id = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
let again = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
assert_eq!(id, again);
let backup = tokio::fs::read_to_string(kimun.join("vault-id.corrupt"))
.await
.unwrap();
assert_eq!(backup, "not-a-uuid");
}
#[tokio::test]
async fn orphaned_empty_file_self_heals() {
let dir = tempfile::TempDir::new().unwrap();
let kimun = dir.path().join(".kimun");
tokio::fs::create_dir_all(&kimun).await.unwrap();
tokio::fs::write(kimun.join("vault-id"), "").await.unwrap();
let id = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
let again = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
assert_eq!(id, again);
}
#[tokio::test]
async fn straggler_settle_never_discards_a_valid_id() {
let dir = tempfile::TempDir::new().unwrap();
let settled = read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap();
let path = vault_id_path(&crate::system::sys(dir.path()));
let straggler = settle_vault_id(&path).unwrap();
assert_eq!(straggler, settled, "settle must adopt the existing id");
assert_eq!(
read_or_create_vault_id(&crate::system::sys(dir.path()))
.await
.unwrap(),
settled
);
}
#[tokio::test]
async fn concurrent_heals_of_a_corrupt_file_converge_on_one_id() {
let dir = tempfile::TempDir::new().unwrap();
let kimun = dir.path().join(".kimun");
tokio::fs::create_dir_all(&kimun).await.unwrap();
tokio::fs::write(kimun.join("vault-id"), "not-a-uuid")
.await
.unwrap();
let workspace = dir.path().to_path_buf();
let handles: Vec<_> = (0..8)
.map(|_| {
let workspace = workspace.clone();
tokio::spawn(async move {
read_or_create_vault_id(&crate::system::sys(workspace))
.await
.unwrap()
})
})
.collect();
let mut ids = Vec::new();
for handle in handles {
ids.push(handle.await.unwrap());
}
assert!(
ids.windows(2).all(|w| w[0] == w[1]),
"all concurrent readers must get the same id: {ids:?}"
);
let on_disk = read_or_create_vault_id(&crate::system::sys(workspace))
.await
.unwrap();
assert_eq!(on_disk, ids[0]);
}
}