pub fn caching<C, P>(inner: C, path: P) -> impl Client<Response = C::Response>
where
C: Client<Response = reqwest::Response>,
P: Into<PathBuf>,
{
struct Caching<C> {
inner: C,
path: PathBuf,
}
#[async_trait]
impl<C> Client for Caching<C>
where
C: Client<Response = reqwest::Response>,
{
type Response = C::Response;
async fn get<U: IntoUrl>(&self, url: U) -> Result<Self::Response> {
let url = url.into_url()?;
let mut cache_path = self.path.clone();
cache_path.push(blake3::hash(url.as_str().as_bytes()).to_hex().as_str());
cache_path.set_extension("http");
let span = debug_span!(
"cached::get",
"url" = url.as_str(),
"cache-path" = cache_path.display().to_string()
);
fn context<'a>(
what: &'static str,
cache_path: &'a Path,
) -> impl FnOnce() -> String + 'a {
move || {
format!(
"failed to open cache file for {what}: {}",
cache_path.display()
)
}
}
if !cache_path.try_exists()? {
span.in_scope(|| debug!("cache miss"));
let resp = self.inner.get(url).await?;
let mut file = tokio::fs::File::create(&cache_path)
.await
.with_context(context("writing", &cache_path))?;
write(resp, &mut file).await?;
file.sync_all().await?;
}
span.in_scope(|| debug!("cache load"));
let file = tokio::fs::File::open(&cache_path)
.await
.with_context(context("reading", &cache_path))?;
read(tokio::io::BufReader::new(file)).await
}
}
Caching {
inner,
path: path.into(),
}
}
async fn write<W: AsyncWrite + Unpin>(resp: reqwest::Response, mut out: W) -> Result<()> {
out.write_all(
format!(
"{:?} {} {}\r\n",
resp.version(),
resp.status().as_u16(),
resp.status().as_str()
)
.as_bytes(),
)
.await?;
for (name, value) in resp.headers() {
out.write_all(name.as_ref()).await?;
out.write_all(b": ").await?;
out.write_all(value.as_bytes()).await?;
out.write_all(b"\r\n").await?;
}
out.write_all(b"\r\n").await?;
let body = resp.bytes().await?;
out.write_all(&body).await?;
Ok(())
}
async fn read<R: AsyncRead + Unpin>(mut io: R) -> Result<reqwest::Response> {
let mut buf = Vec::with_capacity(1024);
io.read_to_end(&mut buf).await?;
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut response = httparse::Response::new(&mut headers);
let httparse::Status::Complete(len) = response.parse(&buf)? else {
bail!("Incomplete response")
};
let body: &[u8] = &buf[len..];
let mut resp = http::Response::builder().status(response.code.unwrap_or(200));
let headers = resp.headers_mut().unwrap();
for header in response.headers {
headers.insert(
HeaderName::from_bytes(header.name.as_bytes())?,
HeaderValue::from_bytes(header.value)?,
);
}
let resp = resp.body(body.to_vec())?;
Ok(resp.into())
}