use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::config::Config;
const FILE_NAME: &str = "codingplan_sync.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SyncMarker {
last_sync_unix_secs: u64,
}
fn marker_path() -> PathBuf {
Config::config_dir().join(FILE_NAME)
}
pub fn read_last_sync() -> Option<SystemTime> {
let path = marker_path();
let bytes = std::fs::read(&path).ok()?;
let marker: SyncMarker = serde_json::from_slice(&bytes).ok()?;
UNIX_EPOCH.checked_add(std::time::Duration::from_secs(marker.last_sync_unix_secs))
}
pub fn write_last_sync_now() -> std::io::Result<()> {
let path = marker_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let marker = SyncMarker {
last_sync_unix_secs: now,
};
let json = serde_json::to_vec_pretty(&marker)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(&path, json)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn env_lock() -> &'static Mutex<()> {
static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct ScopedHome {
_guard: std::sync::MutexGuard<'static, ()>,
prev: Option<String>,
_dir: tempfile::TempDir,
}
impl ScopedHome {
fn new() -> Self {
let guard = env_lock().lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var("ATOMCODE_HOME").ok();
let dir = tempfile::tempdir().expect("tempdir");
std::env::set_var("ATOMCODE_HOME", dir.path());
Self {
_guard: guard,
prev,
_dir: dir,
}
}
}
impl Drop for ScopedHome {
fn drop(&mut self) {
match &self.prev {
Some(v) => std::env::set_var("ATOMCODE_HOME", v),
None => std::env::remove_var("ATOMCODE_HOME"),
}
}
}
#[test]
fn write_then_read_round_trips_timestamp() {
let _home = ScopedHome::new();
write_last_sync_now().expect("write");
let t = read_last_sync().expect("read");
let now = SystemTime::now();
let diff = now
.duration_since(t)
.expect("read timestamp should be <= now");
assert!(
diff.as_secs() < 5,
"round-tripped timestamp should be within 5s of now, got {}s",
diff.as_secs()
);
}
#[test]
fn read_last_sync_returns_none_when_file_absent() {
let _home = ScopedHome::new();
assert!(read_last_sync().is_none());
}
#[test]
fn read_last_sync_returns_none_on_corrupt_json() {
let _home = ScopedHome::new();
let path = marker_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&path, b"not json at all").unwrap();
assert!(read_last_sync().is_none());
}
}