use crate::types::{FlightData, QueryParams, OpenSkyError};
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
const CACHE_DIR_NAME: &str = "opensky";
pub fn cache_dir() -> Option<PathBuf> {
dirs::cache_dir().map(|d| d.join(CACHE_DIR_NAME))
}
pub fn ensure_cache_dir() -> Result<PathBuf, OpenSkyError> {
let dir = cache_dir().ok_or_else(|| {
OpenSkyError::Config("Could not determine cache directory".to_string())
})?;
if !dir.exists() {
fs::create_dir_all(&dir).map_err(|e| {
OpenSkyError::Config(format!("Failed to create cache directory: {}", e))
})?;
}
Ok(dir)
}
pub fn cache_key(params: &QueryParams) -> String {
let mut hasher = DefaultHasher::new();
params.icao24.hash(&mut hasher);
params.start.hash(&mut hasher);
params.stop.hash(&mut hasher);
params.callsign.hash(&mut hasher);
params.departure_airport.hash(&mut hasher);
params.arrival_airport.hash(&mut hasher);
params.airport.hash(&mut hasher);
params.limit.hash(&mut hasher);
if let Some(bounds) = ¶ms.bounds {
bounds.west.to_bits().hash(&mut hasher);
bounds.south.to_bits().hash(&mut hasher);
bounds.east.to_bits().hash(&mut hasher);
bounds.north.to_bits().hash(&mut hasher);
}
let hash = hasher.finish();
format!("{:016x}.parquet", hash)
}
pub fn cache_path(params: &QueryParams) -> Option<PathBuf> {
cache_dir().map(|d| d.join(cache_key(params)))
}
pub fn get_cached(params: &QueryParams, max_age: Option<Duration>) -> Option<FlightData> {
let path = cache_path(params)?;
if !path.exists() {
return None;
}
if let Some(max_age) = max_age {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if let Ok(age) = SystemTime::now().duration_since(modified) {
if age > max_age {
let _ = fs::remove_file(&path);
return None;
}
}
}
}
}
FlightData::from_parquet(&path).ok()
}
pub fn save_to_cache(params: &QueryParams, data: &FlightData) -> Result<PathBuf, OpenSkyError> {
let dir = ensure_cache_dir()?;
let path = dir.join(cache_key(params));
data.to_parquet(&path)?;
Ok(path)
}
pub fn remove_cached(params: &QueryParams) -> Result<(), OpenSkyError> {
if let Some(path) = cache_path(params) {
if path.exists() {
fs::remove_file(&path).map_err(|e| {
OpenSkyError::Config(format!("Failed to remove cache file: {}", e))
})?;
}
}
Ok(())
}
pub fn clear_cache() -> Result<usize, OpenSkyError> {
let dir = match cache_dir() {
Some(d) if d.exists() => d,
_ => return Ok(0),
};
let mut count = 0;
for entry in fs::read_dir(&dir).map_err(|e| {
OpenSkyError::Config(format!("Failed to read cache directory: {}", e))
})? {
if let Ok(entry) = entry {
let path = entry.path();
if path.extension().map_or(false, |e| e == "parquet") {
if fs::remove_file(&path).is_ok() {
count += 1;
}
}
}
}
Ok(count)
}
pub fn purge_old_cache(max_age: Duration) -> Result<usize, OpenSkyError> {
let dir = match cache_dir() {
Some(d) if d.exists() => d,
_ => return Ok(0),
};
let mut count = 0;
let now = SystemTime::now();
for entry in fs::read_dir(&dir).map_err(|e| {
OpenSkyError::Config(format!("Failed to read cache directory: {}", e))
})? {
if let Ok(entry) = entry {
let path = entry.path();
if path.extension().map_or(false, |e| e == "parquet") {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if let Ok(age) = now.duration_since(modified) {
if age > max_age {
if fs::remove_file(&path).is_ok() {
count += 1;
}
}
}
}
}
}
}
}
Ok(count)
}
pub fn cache_stats() -> Result<CacheStats, OpenSkyError> {
let dir = match cache_dir() {
Some(d) => d,
None => return Ok(CacheStats::default()),
};
if !dir.exists() {
return Ok(CacheStats::default());
}
let mut stats = CacheStats {
directory: dir.clone(),
..Default::default()
};
for entry in fs::read_dir(&dir).map_err(|e| {
OpenSkyError::Config(format!("Failed to read cache directory: {}", e))
})? {
if let Ok(entry) = entry {
let path = entry.path();
if path.extension().map_or(false, |e| e == "parquet") {
stats.file_count += 1;
if let Ok(metadata) = fs::metadata(&path) {
stats.total_size += metadata.len();
}
}
}
}
Ok(stats)
}
#[derive(Debug, Default)]
pub struct CacheStats {
pub directory: PathBuf,
pub file_count: usize,
pub total_size: u64,
}
impl CacheStats {
pub fn size_human(&self) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if self.total_size >= GB {
format!("{:.2} GB", self.total_size as f64 / GB as f64)
} else if self.total_size >= MB {
format!("{:.2} MB", self.total_size as f64 / MB as f64)
} else if self.total_size >= KB {
format!("{:.2} KB", self.total_size as f64 / KB as f64)
} else {
format!("{} B", self.total_size)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_key_deterministic() {
let params = QueryParams::new()
.icao24("485a32")
.time_range("2025-01-01 10:00:00", "2025-01-01 12:00:00");
let key1 = cache_key(¶ms);
let key2 = cache_key(¶ms);
assert_eq!(key1, key2);
assert!(key1.ends_with(".parquet"));
}
#[test]
fn test_cache_key_different_params() {
let params1 = QueryParams::new()
.icao24("485a32")
.time_range("2025-01-01 10:00:00", "2025-01-01 12:00:00");
let params2 = QueryParams::new()
.icao24("485a33")
.time_range("2025-01-01 10:00:00", "2025-01-01 12:00:00");
let key1 = cache_key(¶ms1);
let key2 = cache_key(¶ms2);
assert_ne!(key1, key2);
}
}