rdap 0.2.0

A modern RDAP (Registration Data Access Protocol) client
Documentation
//! Disk cache for IANA bootstrap registry files.
//!
//! Caches downloaded bootstrap JSON files (e.g. `dns.json`, `asn.json`) to
//! avoid repeated HTTP requests. Files are stored in the platform-specific
//! cache directory and expire after a configurable TTL (default: 24 hours).

use crate::error::Result;
use directories::ProjectDirs;
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};

/// Manages on-disk caching of bootstrap registry files.
///
/// Files are stored under the platform cache directory
/// (`~/.cache/rdap/` on Linux, `~/Library/Caches/org.openrdap.rdap/` on macOS).
/// Each file is keyed by its IANA filename (e.g. `dns.json`).
pub struct Cache {
    cache_dir: PathBuf,
    ttl: Duration,
}

impl Cache {
    /// Create a new cache with the default 24-hour TTL.
    ///
    /// Creates the cache directory if it does not exist.
    pub fn new() -> Result<Self> {
        let cache_dir = if let Some(proj_dirs) = ProjectDirs::from("org", "openrdap", "rdap") {
            proj_dirs.cache_dir().to_path_buf()
        } else {
            PathBuf::from(".rdap_cache")
        };

        fs::create_dir_all(&cache_dir)?;

        Ok(Self {
            cache_dir,
            ttl: Duration::from_secs(24 * 3600), // 24 hours
        })
    }

    /// Set the cache time-to-live.
    ///
    /// Entries older than `ttl` are considered expired and will be re-fetched.
    /// The default is 24 hours.
    pub fn with_ttl(mut self, ttl: Duration) -> Self {
        self.ttl = ttl;
        self
    }

    /// Return the cached data for `key`, or `None` if missing or expired.
    pub fn get(&self, key: &str) -> Option<Vec<u8>> {
        let path = self.cache_dir.join(key);

        if !path.exists() {
            return None;
        }

        // Check if expired
        if let Ok(metadata) = fs::metadata(&path)
            && let Ok(modified) = metadata.modified()
            && let Ok(elapsed) = SystemTime::now().duration_since(modified)
            && elapsed > self.ttl
        {
            log::debug!("Cache expired for {}", key);
            let _ = fs::remove_file(&path);
            return None;
        }

        fs::read(&path).ok()
    }

    /// Write `data` to the cache under `key`, overwriting any existing entry.
    pub fn set(&self, key: &str, data: &[u8]) -> Result<()> {
        let path = self.cache_dir.join(key);
        fs::write(&path, data)?;
        Ok(())
    }

    /// Remove all cached files.
    pub fn clear(&self) -> Result<()> {
        for entry in fs::read_dir(&self.cache_dir)? {
            let entry = entry?;
            if entry.path().is_file() {
                fs::remove_file(entry.path())?;
            }
        }
        Ok(())
    }
}

/// Creates a [`Cache`] with the default 24-hour TTL.
///
/// # Panics
///
/// Panics if the cache directory cannot be created.
impl Default for Cache {
    fn default() -> Self {
        Self::new().expect("Failed to create cache")
    }
}