use std::path::PathBuf; use anyhow::Context; use tokio::io::AsyncWriteExt; use crate::{app::App, cache_codec::CacheCodecApp, config::Config, git_provider::Repository}; pub struct Cache { app: &'static App, } impl Cache { pub fn new(app: &'static App) -> Self { Self { app } } pub async fn update(&self, repositories: &[Repository]) -> anyhow::Result<()> { tracing::debug!(repository_len = repositories.len(), "storing repositories"); let location = self.app.config.get_cache_file_location()?; tracing::trace!("found cache location: {}", location.display()); if let Some(parent) = location.parent() { tokio::fs::create_dir_all(parent).await?; } let cache_content = self .app .cache_codec() .serialize_repositories(repositories)?; let mut cache_file = tokio::fs::File::create(location) .await .context("failed to create cache file")?; cache_file .write_all(&cache_content) .await .context("failed to write cache content to file")?; Ok(()) } pub async fn get(&self) -> anyhow::Result>> { tracing::debug!("fetching repositories"); let location = self.app.config.get_cache_file_location()?; if !location.exists() { tracing::debug!( location = location.display().to_string(), "cache doesn't exist" ); return Ok(None); } if let Some(cache_duration) = self.app.config.settings.cache.duration.get_duration() { let metadata = tokio::fs::metadata(&location).await?; if let Ok(file_modified_last) = metadata .modified() .context("failed to get modified date") .inspect_err(|e| { tracing::warn!( "could not get valid metadata from file, cache will be reused: {}", e ); }) .and_then(|m| { m.elapsed() .context("failed to get elapsed from file") .inspect_err(|e| tracing::warn!("failed to get elapsed from system: {}", e)) }) { tracing::trace!( cache = file_modified_last.as_secs(), expiry = cache_duration.as_secs(), "checking if cache is valid" ); if file_modified_last > cache_duration { tracing::debug!("cache has expired"); return Ok(None); } tracing::debug!( "cache is valid for: {} mins", cache_duration.saturating_sub(file_modified_last).as_secs() / 60 ); } } let file = tokio::fs::read(&location).await?; if file.is_empty() { tracing::debug!("cache file appears to be empty"); return Ok(None); } let repos = match self.app.cache_codec().deserialize_repositories(file) { Ok(repos) => repos, Err(e) => { tracing::warn!(error = e.to_string(), "failed to deserialize repositories"); return Ok(None); } }; Ok(Some(repos)) } } pub trait CacheApp { fn cache(&self) -> Cache; } impl CacheApp for &'static App { fn cache(&self) -> Cache { Cache::new(self) } } pub trait CacheConfig { fn get_cache_location(&self) -> anyhow::Result; fn get_cache_file_location(&self) -> anyhow::Result; } impl CacheConfig for Config { fn get_cache_location(&self) -> anyhow::Result { Ok(self.settings.cache.location.clone().into()) } fn get_cache_file_location(&self) -> anyhow::Result { Ok(self.get_cache_location()?.join("cache.proto")) } }