use std::path::{Path, PathBuf};
use crate::service::colocated_storage::COLOCATED_DIR_NAME;
use crate::service::persistence::data_dir;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IndexMigrationClass {
AlreadyColocated,
NeedsMigration {
src_dir: PathBuf,
dst_dir: PathBuf,
},
LegacyPointerFile {
pointer_path: PathBuf,
src_dir: PathBuf,
dst_dir: PathBuf,
},
SkipDeadRoot,
SkipNoData,
}
pub fn classify_index(index_id: &str, root_path: &Path) -> IndexMigrationClass {
if !root_path.exists() || !root_path.is_dir() {
return IndexMigrationClass::SkipDeadRoot;
}
let colocated_candidate = root_path.join(COLOCATED_DIR_NAME);
let dst_dir = colocated_candidate.clone();
if colocated_candidate.is_dir() {
let redb_in_colocated = colocated_candidate.join("index.redb");
if is_populated_file(&redb_in_colocated) {
return IndexMigrationClass::AlreadyColocated;
}
} else if colocated_candidate.exists() {
if let Ok(src_dir) = app_data_index_dir(index_id) {
return IndexMigrationClass::LegacyPointerFile {
pointer_path: colocated_candidate,
src_dir,
dst_dir,
};
}
return IndexMigrationClass::SkipNoData;
}
if let Ok(src_dir) = app_data_index_dir(index_id) {
let redb_in_src = src_dir.join("index.redb");
if is_populated_file(&redb_in_src) {
return IndexMigrationClass::NeedsMigration { src_dir, dst_dir };
}
}
IndexMigrationClass::SkipNoData
}
pub(super) fn app_data_index_dir(index_id: &str) -> anyhow::Result<PathBuf> {
let sanitized: String = index_id
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect();
Ok(data_dir()?.join("indexes").join(sanitized))
}
pub(super) fn is_populated_file(path: &Path) -> bool {
match std::fs::metadata(path) {
Ok(m) => m.is_file() && m.len() > 0,
Err(_) => false,
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::service::persistence::data_dir;
use serial_test::serial;
use tempfile::tempdir;
#[test]
#[serial]
fn classify_already_colocated() {
let data_tmp = tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
let root = tempdir().unwrap();
let ts_dir = root.path().join(".trusty-search");
std::fs::create_dir_all(&ts_dir).unwrap();
std::fs::write(ts_dir.join("index.redb"), b"notempty").unwrap();
let result = classify_index("test-idx", root.path());
assert_eq!(result, IndexMigrationClass::AlreadyColocated);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[test]
#[serial]
fn classify_needs_migration_no_colocated_dir() {
let data_tmp = tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
let root = tempdir().unwrap();
let src = data_dir().unwrap().join("indexes").join("myidx");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("index.redb"), b"data").unwrap();
let result = classify_index("myidx", root.path());
match result {
IndexMigrationClass::NeedsMigration { src_dir, dst_dir } => {
assert!(src_dir.ends_with("indexes/myidx"));
assert!(dst_dir.ends_with(".trusty-search"));
}
other => panic!("expected NeedsMigration, got {other:?}"),
}
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[test]
#[serial]
fn classify_legacy_pointer_file_is_not_already_colocated() {
let data_tmp = tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
let root = tempdir().unwrap();
let pointer = root.path().join(".trusty-search");
std::fs::write(&pointer, b"index = \"my-project\"").unwrap();
assert!(pointer.exists() && pointer.is_file());
let src = data_dir().unwrap().join("indexes").join("ptr-test");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("index.redb"), b"data").unwrap();
let result = classify_index("ptr-test", root.path());
match &result {
IndexMigrationClass::LegacyPointerFile {
pointer_path,
src_dir,
dst_dir,
} => {
assert_eq!(pointer_path, &pointer);
assert!(src_dir.ends_with("indexes/ptr-test"));
assert!(dst_dir.ends_with(".trusty-search"));
}
other => panic!("expected LegacyPointerFile (not AlreadyColocated), got {other:?}"),
}
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[test]
#[serial]
fn classify_dead_root() {
let data_tmp = tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
let missing = PathBuf::from("/tmp/trusty-classify-dead-root-12345");
let result = classify_index("ghost", &missing);
assert_eq!(result, IndexMigrationClass::SkipDeadRoot);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[test]
#[serial]
fn classify_no_data() {
let data_tmp = tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
let root = tempdir().unwrap();
let result = classify_index("empty-idx", root.path());
assert_eq!(result, IndexMigrationClass::SkipNoData);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
}