use std::{
collections::hash_map::DefaultHasher,
ffi::OsStr,
fmt::Debug,
fs,
hash::{Hash, Hasher},
io,
path::{Path, PathBuf},
};
use anyhow::{Error, Result};
use http::Uri;
pub type BoxedHttpCache = Box<dyn HttpCache + Send + Sync>;
pub trait HttpCache: HttpCacheClone + Debug {
fn cache_response(
&self,
uri: &str,
body: &[u8],
etag: &[u8],
next_link: &Option<String>,
) -> Result<()>;
fn lookup_etag(&self, uri: &str) -> Result<String>;
fn lookup_body(&self, uri: &str) -> Result<String>;
fn lookup_next_link(&self, uri: &str) -> Result<Option<String>>;
}
impl dyn HttpCache {
pub fn noop() -> BoxedHttpCache {
Box::new(NoCache)
}
pub fn in_home_dir() -> BoxedHttpCache {
let mut dir = dirs::home_dir().expect("Expected a home dir");
dir.push(".github/cache");
Box::new(FileBasedCache::new(dir))
}
}
impl Clone for BoxedHttpCache {
fn clone(&self) -> Self {
self.box_clone()
}
}
#[derive(Clone, Debug)]
pub struct NoCache;
impl HttpCache for NoCache {
fn cache_response(&self, _: &str, _: &[u8], _: &[u8], _: &Option<String>) -> Result<()> {
Ok(())
}
fn lookup_etag(&self, _uri: &str) -> Result<String> {
no_read("No etag cached")
}
fn lookup_body(&self, _uri: &str) -> Result<String> {
no_read("No body cached")
}
fn lookup_next_link(&self, _uri: &str) -> Result<Option<String>> {
no_read("No next link cached")
}
}
#[derive(Clone, Debug)]
pub struct FileBasedCache {
root: PathBuf,
}
impl FileBasedCache {
#[doc(hidden)] pub fn new<P: Into<PathBuf>>(root: P) -> FileBasedCache {
FileBasedCache { root: root.into() }
}
}
impl HttpCache for FileBasedCache {
fn cache_response(
&self,
uri: &str,
body: &[u8],
etag: &[u8],
next_link: &Option<String>,
) -> Result<()> {
let mut path = cache_path(&self.root, uri, "json");
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, body)?;
path.set_extension("etag");
fs::write(&path, etag)?;
if let Some(next_link) = next_link {
path.set_extension("next_link");
fs::write(&path, next_link)?;
}
Ok(())
}
fn lookup_etag(&self, uri: &str) -> Result<String> {
read_to_string(cache_path(&self.root, uri, "etag"))
}
fn lookup_body(&self, uri: &str) -> Result<String> {
read_to_string(cache_path(&self.root, uri, "json"))
}
fn lookup_next_link(&self, uri: &str) -> Result<Option<String>> {
let path = cache_path(&self.root, uri, "next_link");
if path.exists() {
Ok(Some(read_to_string(path)?))
} else {
Ok(None)
}
}
}
#[doc(hidden)] pub fn cache_path<S: AsRef<OsStr>>(dir: &Path, uri: &str, extension: S) -> PathBuf {
let uri_encoded = uri.replace(" ", "%20");
let uri = uri_encoded
.parse::<Uri>()
.unwrap_or_else(|_| panic!("Expected a URI, got {}", uri_encoded));
let parts = uri.clone().into_parts();
let mut path = dir.to_path_buf();
path.push("v1");
path.push(parts.scheme.expect("no URI scheme").as_str()); path.push(parts.authority.expect("no URI authority").as_str()); path.push(Path::new(&uri.path()[1..])); if let Some(query) = uri.query() {
path.push(hash1(query, DefaultHasher::new())); }
path.set_extension(extension); path
}
fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
fs::read_to_string(path).map_err(Error::from)
}
fn no_read<T, E: Into<Box<dyn std::error::Error + Send + Sync>>>(error: E) -> Result<T> {
Err(Error::from(io::Error::new(io::ErrorKind::NotFound, error)))
}
#[doc(hidden)]
pub trait HttpCacheClone {
#[doc(hidden)]
fn box_clone(&self) -> BoxedHttpCache;
}
impl<T> HttpCacheClone for T
where
T: 'static + HttpCache + Clone + Send + Sync,
{
fn box_clone(&self) -> BoxedHttpCache {
Box::new(self.clone())
}
}
fn hash1<A: Hash, H: Hasher>(x: A, mut hasher: H) -> String {
x.hash(&mut hasher);
u64_to_padded_hex(hasher.finish())
}
#[doc(hidden)] pub fn u64_to_padded_hex(x: u64) -> String {
format!("{:016x}", x)
}