use std::path::PathBuf;
const THINGS_GROUP_CONTAINER: &str =
"Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac";
const THINGS_DB_RELATIVE: &str = "Things Database.thingsdatabase/main.sqlite";
#[must_use]
pub fn get_default_database_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
let group_container = PathBuf::from(&home).join(THINGS_GROUP_CONTAINER);
if let Some(found) = discover_things_database(&group_container) {
return found;
}
group_container
.join("ThingsData-0Z0Z2")
.join(THINGS_DB_RELATIVE)
}
fn discover_things_database(group_container: &std::path::Path) -> Option<PathBuf> {
let entries = std::fs::read_dir(group_container).ok()?;
let mut best: Option<(PathBuf, std::time::SystemTime)> = None;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name_str) = name.to_str() else {
continue;
};
if !name_str.starts_with("ThingsData-") {
continue;
}
let candidate = entry.path().join(THINGS_DB_RELATIVE);
let Ok(meta) = std::fs::metadata(&candidate) else {
continue;
};
if !meta.is_file() {
continue;
}
let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
match &best {
Some((_, best_mtime)) if mtime <= *best_mtime => {}
_ => best = Some((candidate, mtime)),
}
}
best.map(|(path, _)| path)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_get_default_database_path_format() {
let path = get_default_database_path();
let path_str = path.to_string_lossy();
assert!(path_str.contains("Things Database.thingsdatabase"));
assert!(path_str.contains("main.sqlite"));
assert!(path_str.contains("Library/Group Containers"));
}
#[test]
fn test_discover_things_database_picks_non_default_suffix() {
let group_container = TempDir::new().unwrap();
let things_dir = group_container.path().join("ThingsData-01AEF");
let db_dir = things_dir.join("Things Database.thingsdatabase");
std::fs::create_dir_all(&db_dir).unwrap();
let db_path = db_dir.join("main.sqlite");
std::fs::write(&db_path, b"").unwrap();
let found = discover_things_database(group_container.path()).unwrap();
assert_eq!(found, db_path);
}
#[test]
fn test_discover_things_database_prefers_most_recent() {
let group_container = TempDir::new().unwrap();
let make = |suffix: &str| {
let dir = group_container
.path()
.join(format!("ThingsData-{suffix}"))
.join("Things Database.thingsdatabase");
std::fs::create_dir_all(&dir).unwrap();
let db = dir.join("main.sqlite");
std::fs::write(&db, b"").unwrap();
db
};
let _older = make("OLDER");
std::thread::sleep(std::time::Duration::from_millis(10));
let newer = make("NEWER");
let found = discover_things_database(group_container.path()).unwrap();
assert_eq!(found, newer);
}
#[test]
fn test_discover_things_database_returns_none_when_empty() {
let group_container = TempDir::new().unwrap();
assert!(discover_things_database(group_container.path()).is_none());
}
#[test]
fn test_discover_things_database_skips_non_matching_dirs() {
let group_container = TempDir::new().unwrap();
std::fs::create_dir_all(group_container.path().join("SomethingElse")).unwrap();
std::fs::create_dir_all(
group_container.path().join("ThingsData-EMPTY"), )
.unwrap();
assert!(discover_things_database(group_container.path()).is_none());
}
}