Files
gitlore/src/core/paths.rs
teernisse eac640225f feat(core): add cursor persistence module for session-based timestamps
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>
2026-02-25 10:02:13 -05:00

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"));
}
}