use crate::api::client::Client;
use crate::api::orgs::validators;
use crate::api::projects;
use crate::config::build_config;
use crate::error::Error;
use crate::project::Project;
use chrono::Duration;
use chrono::{DateTime, Utc};
use eyre::{ContextCompat, WrapErr};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
const CACHE_EXPIRES_IN: Duration = Duration::minutes(10);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct Cache {
pub(super) projects: HashMap<String, Project>,
last_updated: DateTime<Utc>,
}
impl Cache {
pub(super) async fn new(org: Option<&str>) -> eyre::Result<Self> {
let cache_path = Self::path(org)?;
let cache: Option<Self> = if !cache_path.exists() {
if let Some(parent) = cache_path.parent() {
fs::create_dir_all(parent)
.inspect_err(|e| {
log::error!("Failed to create cache directory {parent:?}: {e:?}")
})
.wrap_err("Failed to create project cache")?;
}
None
} else {
let cache_content = fs::read_to_string(&cache_path)
.inspect_err(|e| log::error!("Failed to read cache file {cache_path:?}: {e:?}"))
.wrap_err("Failed to load project cache")?;
match serde_json::from_str(&cache_content) {
Ok::<Cache, _>(cache) if Utc::now() - cache.last_updated < CACHE_EXPIRES_IN => {
Some(cache)
}
_ => None,
}
};
let cache = match cache {
Some(x) => x,
None => Self::load(org).await?,
};
let cache_json = serde_json::to_string_pretty(&cache)
.inspect_err(|e| log::error!("Failed to serialize project cache: {e:?}"))
.wrap_err("Failed to process cache")?;
let cache_path = Self::path(org)?;
fs::write(&cache_path, cache_json)
.inspect_err(|e| log::error!("Failed to write cache file {cache_path:?}: {e:?}"))
.wrap_err("Failed to write cache")?;
Ok(cache)
}
pub(super) fn get(&self, project_name: &str) -> eyre::Result<Project> {
self.projects
.get(project_name)
.wrap_err("Project not found")
.cloned()
}
pub(super) fn clear() -> eyre::Result<()> {
let cache_dir = PathBuf::from(build_config()?.kinetics_path);
if !cache_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(&cache_dir)
.inspect_err(|e| log::error!("Failed to read cache directory {cache_dir:?}: {e:?}"))
.wrap_err("Failed to clear the projects cache")?
{
let entry = entry
.inspect_err(|e| log::error!("Failed to read cache entry in {cache_dir:?}: {e:?}"))
.wrap_err("Failed to clear the projects cache")?;
let path = entry.path();
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if file_name.starts_with(".projects") {
fs::remove_file(&path)
.inspect_err(|e| log::error!("Failed to remove cache file {path:?}: {e:?}"))
.wrap_err("Failed to clear the projects cache")?;
}
}
Ok(())
}
fn path(org: Option<&str>) -> eyre::Result<PathBuf> {
let file_name = match org {
Some(org) => {
if !validators::Name::validate(org) {
return Err(eyre::eyre!(validators::Name::message()));
}
format!(".projects.{org}")
}
None => ".projects".to_string(),
};
Ok(PathBuf::from(build_config()?.kinetics_path).join(file_name))
}
async fn load(org: Option<&str>) -> eyre::Result<Self> {
let response = Client::new(false)
.await?
.request::<projects::Request, projects::Response>(
"/projects",
projects::Request {
org: org.map(str::to_owned),
},
)
.await
.wrap_err(Error::new(
"Failed to fetch project information",
Some("Try again in a few seconds."),
))?;
let projects = HashMap::from_iter(
response
.projects
.into_iter()
.map(|project| (project.name.clone(), project.into())),
);
Ok(Self {
projects,
last_updated: Utc::now(),
})
}
}