use crate::system::{self, SystemError, SystemPath};
const INDEX_FILE_EXT: &str = "kimuncache";
const SIDECAR_SUFFIXES: [&str; 3] = ["-wal", "-shm", "-journal"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexFile {
path: SystemPath,
}
impl IndexFile {
pub fn in_dir(dir: &SystemPath, workspace_name: &str) -> Self {
Self {
path: dir.join(format!("{workspace_name}.{INDEX_FILE_EXT}")),
}
}
pub fn at(path: SystemPath) -> Self {
Self { path }
}
pub fn legacy_in_workspace(workspace_path: &SystemPath) -> Self {
Self {
path: workspace_path.join(super::DB_FILE),
}
}
pub fn path(&self) -> &SystemPath {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
fn existing_sidecars(&self) -> Vec<(&'static str, SystemPath)> {
SIDECAR_SUFFIXES
.iter()
.map(|suffix| (*suffix, self.path.with_name_suffix(suffix)))
.filter(|(_, path)| path.exists())
.collect()
}
pub fn move_to(&self, dest: &IndexFile) -> Result<(), SystemError> {
let sidecars = self.existing_sidecars();
if !self.exists() && sidecars.is_empty() {
return Ok(());
}
if dest.exists() {
return Err(SystemError::AlreadyExists {
path: dest.path.to_string(),
});
}
for suffix in SIDECAR_SUFFIXES {
let occupied = dest.path.with_name_suffix(suffix);
if occupied.exists() {
return Err(SystemError::AlreadyExists {
path: occupied.to_string(),
});
}
}
let mut plan = Vec::new();
if self.exists() {
plan.push((self.path.clone(), dest.path.clone()));
}
for (suffix, source) in sidecars {
plan.push((source, dest.path.with_name_suffix(suffix)));
}
move_all(&plan)
}
pub fn remove(&self) -> Vec<(SystemPath, SystemError)> {
let mut stuck = Vec::new();
for (_, sidecar) in self.existing_sidecars() {
if let Err(e) = system::remove_file(sidecar.as_path()) {
stuck.push((sidecar, e));
}
}
if self.exists() {
if let Err(e) = system::remove_file(self.path.as_path()) {
stuck.push((self.path.clone(), e));
}
}
stuck
}
}
impl std::fmt::Display for IndexFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)
}
}
fn move_all(pairs: &[(SystemPath, SystemPath)]) -> Result<(), SystemError> {
let mut done = Vec::new();
for (from, to) in pairs {
match system::move_file(from.as_path(), to.as_path()) {
Ok(()) => done.push((from, to)),
Err(e) => {
for (from, to) in done.iter().rev() {
let _ = system::move_file(to.as_path(), from.as_path());
}
return Err(e);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::sys;
fn index_in(dir: &tempfile::TempDir, name: &str) -> IndexFile {
IndexFile::in_dir(&sys(dir.path()), name)
}
fn write(path: &SystemPath, body: &str) {
std::fs::write(path.as_path(), body).unwrap();
}
#[test]
fn in_dir_names_the_file_after_the_workspace() {
let dir = tempfile::TempDir::new().unwrap();
let index = index_in(&dir, "work");
assert_eq!(
index.path().as_path().file_name().unwrap(),
"work.kimuncache"
);
}
#[test]
fn move_takes_the_sidecars_along() {
let dir = tempfile::TempDir::new().unwrap();
let from = index_in(&dir, "old");
let to = index_in(&dir, "new");
write(from.path(), "index");
write(&from.path().with_name_suffix("-wal"), "wal");
write(&from.path().with_name_suffix("-shm"), "shm");
from.move_to(&to).unwrap();
assert!(!from.exists());
assert!(!from.path().with_name_suffix("-wal").exists());
assert!(!from.path().with_name_suffix("-shm").exists());
assert_eq!(
std::fs::read_to_string(to.path().as_path()).unwrap(),
"index"
);
assert_eq!(
std::fs::read_to_string(to.path().with_name_suffix("-wal").as_path()).unwrap(),
"wal"
);
assert_eq!(
std::fs::read_to_string(to.path().with_name_suffix("-shm").as_path()).unwrap(),
"shm"
);
}
#[test]
fn move_of_a_missing_index_is_a_no_op() {
let dir = tempfile::TempDir::new().unwrap();
let from = index_in(&dir, "absent");
let to = index_in(&dir, "new");
from.move_to(&to).unwrap();
assert!(!to.exists());
}
#[test]
fn move_takes_a_stranded_sidecar_with_no_main_file() {
let dir = tempfile::TempDir::new().unwrap();
let from = index_in(&dir, "old");
let to = index_in(&dir, "new");
write(&from.path().with_name_suffix("-wal"), "orphan wal");
from.move_to(&to).unwrap();
assert!(
!from.path().with_name_suffix("-wal").exists(),
"the stale WAL must not stay under the old name"
);
assert_eq!(
std::fs::read_to_string(to.path().with_name_suffix("-wal").as_path()).unwrap(),
"orphan wal"
);
assert!(!to.exists(), "no main file was invented");
}
#[test]
fn each_sidecar_keeps_its_suffix_across_a_move() {
let dir = tempfile::TempDir::new().unwrap();
let from = index_in(&dir, "old");
let to = index_in(&dir, "new");
write(from.path(), "index");
for suffix in SIDECAR_SUFFIXES {
write(&from.path().with_name_suffix(suffix), suffix);
}
from.move_to(&to).unwrap();
assert_eq!(
std::fs::read_to_string(to.path().as_path()).unwrap(),
"index",
"the index must not be overwritten by a sidecar"
);
for suffix in SIDECAR_SUFFIXES {
assert_eq!(
std::fs::read_to_string(to.path().with_name_suffix(suffix).as_path()).unwrap(),
suffix
);
}
}
#[test]
fn move_refuses_an_occupied_destination() {
let dir = tempfile::TempDir::new().unwrap();
let from = index_in(&dir, "old");
let to = index_in(&dir, "new");
write(from.path(), "source");
write(to.path(), "destination");
let err = from.move_to(&to).unwrap_err();
assert!(
matches!(err, SystemError::AlreadyExists { .. }),
"got {err:?}"
);
assert_eq!(
std::fs::read_to_string(to.path().as_path()).unwrap(),
"destination",
"an existing index must not be overwritten"
);
assert!(from.exists(), "source must be left alone");
}
#[test]
fn move_refuses_an_occupied_destination_sidecar() {
let dir = tempfile::TempDir::new().unwrap();
let from = index_in(&dir, "old");
let to = index_in(&dir, "new");
write(from.path(), "source");
write(&to.path().with_name_suffix("-wal"), "stale wal");
let err = from.move_to(&to).unwrap_err();
assert!(
matches!(err, SystemError::AlreadyExists { .. }),
"got {err:?}"
);
assert!(from.exists(), "source must be left alone");
}
#[test]
fn a_failed_move_puts_the_earlier_ones_back() {
let dir = tempfile::TempDir::new().unwrap();
let main = sys(dir.path()).join("index.kimuncache");
let wal = main.with_name_suffix("-wal");
write(&main, "index");
write(&wal, "wal");
let moved_main = sys(dir.path()).join("moved.kimuncache");
let unreachable = sys(dir.path())
.join("no-such-dir")
.join("moved.kimuncache-wal");
let result = move_all(&[
(main.clone(), moved_main.clone()),
(wal.clone(), unreachable),
]);
assert!(result.is_err(), "the second move must fail");
assert!(main.exists(), "index must be back where it started");
assert!(wal.exists(), "WAL must be back where it started");
assert!(!moved_main.exists(), "no half-moved index left behind");
assert_eq!(std::fs::read_to_string(main.as_path()).unwrap(), "index");
}
#[test]
fn remove_deletes_the_sidecars_too() {
let dir = tempfile::TempDir::new().unwrap();
let index = index_in(&dir, "doomed");
write(index.path(), "index");
write(&index.path().with_name_suffix("-wal"), "wal");
write(&index.path().with_name_suffix("-shm"), "shm");
assert!(index.remove().is_empty());
assert!(!index.exists());
assert!(!index.path().with_name_suffix("-wal").exists());
assert!(!index.path().with_name_suffix("-shm").exists());
let leftovers: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert!(leftovers.is_empty(), "left behind: {leftovers:?}");
}
#[test]
fn remove_of_a_missing_index_is_a_no_op() {
let dir = tempfile::TempDir::new().unwrap();
assert!(index_in(&dir, "absent").remove().is_empty());
}
#[test]
fn remove_attempts_every_file_and_reports_each_failure() {
let dir = tempfile::TempDir::new().unwrap();
let index = index_in(&dir, "stuck");
let wal = index.path().with_name_suffix("-wal");
let shm = index.path().with_name_suffix("-shm");
write(index.path(), "index");
write(&shm, "shm");
std::fs::create_dir(wal.as_path()).unwrap();
std::fs::write(wal.as_path().join("occupied"), b"x").unwrap();
let stuck = index.remove();
assert!(!index.exists(), "the index itself was still deletable");
assert!(!shm.exists(), "the -shm was still deletable");
assert_eq!(stuck.len(), 1, "got {stuck:?}");
assert_eq!(stuck[0].0, wal);
}
}