Introduces a lightweight file-based cursor system for persisting per-user timestamps across CLI invocations. This enables "since last check" semantics where `lore me` can track what the user has seen. Key design decisions: - Per-user cursor files: ~/.local/share/lore/me_cursor_<username>.json - Atomic writes via temp-file + rename pattern (crash-safe) - Graceful degradation: missing/corrupt files return None - Username sanitization: non-safe chars replaced with underscore The cursor module provides three operations: - read_cursor(username) -> Option<i64>: read last-check timestamp - write_cursor(username, timestamp_ms): atomically persist timestamp - reset_cursor(username): delete cursor file (no-op if missing) Tests cover: missing file, roundtrip, per-user isolation, reset isolation, JSON validity after overwrites, corrupt file handling. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.2 KiB
Rust
125 lines
3.2 KiB
Rust
use std::path::PathBuf;
|
|
|
|
pub fn get_config_path(cli_override: Option<&str>) -> PathBuf {
|
|
if let Some(path) = cli_override {
|
|
return PathBuf::from(path);
|
|
}
|
|
|
|
if let Ok(path) = std::env::var("LORE_CONFIG_PATH") {
|
|
return PathBuf::from(path);
|
|
}
|
|
|
|
let xdg_path = get_xdg_config_dir().join("lore").join("config.json");
|
|
if xdg_path.exists() {
|
|
return xdg_path;
|
|
}
|
|
|
|
let local_path = PathBuf::from("lore.config.json");
|
|
if local_path.exists() {
|
|
return local_path;
|
|
}
|
|
|
|
xdg_path
|
|
}
|
|
|
|
pub fn get_data_dir() -> PathBuf {
|
|
get_xdg_data_dir().join("lore")
|
|
}
|
|
|
|
pub fn get_db_path(config_override: Option<&str>) -> PathBuf {
|
|
if let Some(path) = config_override {
|
|
return PathBuf::from(path);
|
|
}
|
|
get_data_dir().join("lore.db")
|
|
}
|
|
|
|
pub fn get_log_dir(config_override: Option<&str>) -> PathBuf {
|
|
if let Some(path) = config_override {
|
|
return PathBuf::from(path);
|
|
}
|
|
get_data_dir().join("logs")
|
|
}
|
|
|
|
pub fn get_cursor_path(username: &str) -> PathBuf {
|
|
let safe_username: String = username
|
|
.chars()
|
|
.map(|ch| {
|
|
if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
|
|
ch
|
|
} else {
|
|
'_'
|
|
}
|
|
})
|
|
.collect();
|
|
get_data_dir().join(format!("me_cursor_{safe_username}.json"))
|
|
}
|
|
|
|
pub fn get_backup_dir(config_override: Option<&str>) -> PathBuf {
|
|
if let Some(path) = config_override {
|
|
return PathBuf::from(path);
|
|
}
|
|
get_data_dir().join("backups")
|
|
}
|
|
|
|
fn get_xdg_config_dir() -> PathBuf {
|
|
std::env::var("XDG_CONFIG_HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|_| {
|
|
dirs::home_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join(".config")
|
|
})
|
|
}
|
|
|
|
fn get_xdg_data_dir() -> PathBuf {
|
|
std::env::var("XDG_DATA_HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|_| {
|
|
dirs::home_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join(".local")
|
|
.join("share")
|
|
})
|
|
}
|
|
|
|
/// Enforce restrictive permissions (0600) on the config file.
|
|
/// Warns to stderr if permissions were too open, then tightens them.
|
|
#[cfg(unix)]
|
|
pub fn ensure_config_permissions(path: &std::path::Path) {
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
let Ok(meta) = std::fs::metadata(path) else {
|
|
return;
|
|
};
|
|
let mode = meta.mode() & 0o777;
|
|
if mode != 0o600 {
|
|
eprintln!(
|
|
"Warning: config file permissions were {mode:04o}, tightening to 0600: {}",
|
|
path.display()
|
|
);
|
|
let _ = set_permissions_600(path);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn set_permissions_600(path: &std::path::Path) -> std::io::Result<()> {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let perms = std::fs::Permissions::from_mode(0o600);
|
|
std::fs::set_permissions(path, perms)
|
|
}
|
|
|
|
/// No-op on non-Unix platforms.
|
|
#[cfg(not(unix))]
|
|
pub fn ensure_config_permissions(_path: &std::path::Path) {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_cli_override_takes_precedence() {
|
|
let path = get_config_path(Some("/custom/path.json"));
|
|
assert_eq!(path, PathBuf::from("/custom/path.json"));
|
|
}
|
|
}
|