#![doc(html_root_url = "https://docs.rs/static_http_cache/0.3.0")]
use std::error;
use std::fs;
use std::io;
use std::path;
use log::{debug, info, warn};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use reqwest::header as rh;
pub mod reqwest_mock;
mod db;
fn make_random_file<P: AsRef<path::Path>>(
parent: P,
) -> Result<(fs::File, path::PathBuf), Box<dyn error::Error>> {
let mut rng = thread_rng();
loop {
let new_path = parent.as_ref().join(
(0..20)
.map(|_| rng.sample(Alphanumeric) as char)
.collect::<String>(),
);
match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&new_path)
{
Ok(handle) => return Ok((handle, new_path)),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
};
}
}
fn header_as_string(
headers: &rh::HeaderMap,
key: &rh::HeaderName,
) -> Option<String> {
headers.get(key).and_then(|value| match value.to_str() {
Ok(s) => Some(s.into()),
Err(err) => {
warn!("Header {} contained weird value: {}", key, err);
None
}
})
}
#[derive(Debug, PartialEq, Eq)]
pub struct Cache<C: reqwest_mock::Client> {
root: path::PathBuf,
db: db::CacheDB,
client: C,
}
impl<C: reqwest_mock::Client> Cache<C> {
pub fn new(
root: path::PathBuf,
client: C,
) -> Result<Cache<C>, Box<dyn error::Error>> {
fs::DirBuilder::new().recursive(true).create(&root)?;
let db = db::CacheDB::new(root.join("cache.db"))?;
Ok(Cache { root, db, client })
}
fn record_response(
&mut self,
url: reqwest::Url,
response: &C::Response,
) -> Result<(fs::File, path::PathBuf, db::Transaction), Box<dyn error::Error>>
{
use reqwest_mock::HttpResponse;
let content_dir = self.root.join("content");
fs::DirBuilder::new().recursive(true).create(&content_dir)?;
let (handle, path) = make_random_file(&content_dir)?;
let trans = {
let path = path.strip_prefix(&self.root)?.to_str().unwrap().into();
let last_modified =
header_as_string(response.headers(), &rh::LAST_MODIFIED);
let etag = header_as_string(response.headers(), &rh::ETAG);
self.db.set(
url,
db::CacheRecord {
path,
last_modified,
etag,
},
)?
};
Ok((handle, path, trans))
}
pub fn get(
&mut self,
mut url: reqwest::Url,
) -> Result<fs::File, Box<dyn error::Error>> {
use reqwest::StatusCode;
use reqwest_mock::HttpResponse;
url.set_fragment(None);
let mut response = match self.db.get(url.clone()) {
Ok(db::CacheRecord {
path: p,
last_modified: lm,
etag: et,
}) => {
let mut request = reqwest::blocking::Request::new(
reqwest::Method::GET,
url.clone(),
);
if let Some(timestamp) = lm {
request.headers_mut().append(
rh::IF_MODIFIED_SINCE,
rh::HeaderValue::from_str(×tamp)?,
);
}
if let Some(etag) = et {
request.headers_mut().append(
rh::IF_NONE_MATCH,
rh::HeaderValue::from_str(&etag)?,
);
}
info!("Sending HTTP request: {:?}", request);
let maybe_validation = self
.client
.execute(request)
.and_then(|resp| resp.error_for_status());
match maybe_validation {
Ok(new_response) => {
info!("Got HTTP response: {:?}", new_response);
if new_response.status() == StatusCode::NOT_MODIFIED {
return Ok(fs::File::open(self.root.join(p))?);
}
new_response
}
Err(e) => {
warn!("Could not validate cached response: {}", e);
return Ok(fs::File::open(self.root.join(p))?);
}
}
}
Err(_) => {
self.client
.execute(reqwest::blocking::Request::new(
reqwest::Method::GET,
url.clone(),
))?
.error_for_status()?
}
};
let (mut handle, path, trans) =
self.record_response(url.clone(), &response)?;
let count = io::copy(&mut response, &mut handle)?;
debug!("Downloaded {} bytes", count);
trans.commit()?;
Ok(fs::File::open(path)?)
}
}
#[cfg(test)]
mod tests {
extern crate env_logger;
extern crate tempdir;
use reqwest::header as rh;
use std::io;
use std::io::Read;
use super::reqwest_mock::tests as rmt;
const DATE_ZERO: &str = "Thu, 01 Jan 1970 00:00:00 GMT";
const DATE_ONE: &str = "Thu, 01 Jan 1970 00:00:00 GMT";
fn make_test_cache(
client: rmt::FakeClient,
) -> super::Cache<rmt::FakeClient> {
super::Cache::new(
tempdir::TempDir::new("http-cache-test")
.unwrap()
.into_path(),
client,
)
.unwrap()
}
#[test]
fn initial_request_success() {
let _ = env_logger::try_init();
let url_text = "http://example.com/";
let url: reqwest::Url = url_text.parse().unwrap();
let body = b"hello world";
let mut c = make_test_cache(rmt::FakeClient::new(
url.clone(),
rh::HeaderMap::new(),
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: rh::HeaderMap::new(),
body: io::Cursor::new(body.as_ref().into()),
},
));
let mut res = c.get(url).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, body);
c.client.assert_called();
}
#[test]
fn initial_request_failure() {
let _ = env_logger::try_init();
let url: reqwest::Url = "http://example.com/".parse().unwrap();
let mut c = make_test_cache(rmt::FakeClient::new(
url.clone(),
rh::HeaderMap::new(),
rmt::FakeResponse {
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
headers: rh::HeaderMap::new(),
body: io::Cursor::new(vec![]),
},
));
let err = c.get(url).expect_err("Got a response??");
assert_eq!(format!("{}", err), "FakeError");
c.client.assert_called();
}
#[test]
fn ignore_fragment_in_url() {
let _ = env_logger::try_init();
let url_fragment: reqwest::Url =
"http://example.com/#frag".parse().unwrap();
let mut network_url = url_fragment.clone();
network_url.set_fragment(None);
let mut c = make_test_cache(rmt::FakeClient::new(
network_url,
rh::HeaderMap::new(),
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: rh::HeaderMap::new(),
body: io::Cursor::new(b"hello world"[..].into()),
},
));
c.get(url_fragment).unwrap();
}
#[test]
fn use_cache_data_if_not_modified_since() {
let _ = env_logger::try_init();
let url: reqwest::Url = "http://example.com/".parse().unwrap();
let body = b"hello world";
let mut response_headers = rh::HeaderMap::new();
response_headers
.append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ZERO));
let mut c = make_test_cache(rmt::FakeClient::new(
url.clone(),
rh::HeaderMap::new(),
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_headers.clone(),
body: io::Cursor::new(body.as_ref().into()),
},
));
c.get(url.clone()).unwrap();
c.client.assert_called();
let mut second_request = rh::HeaderMap::new();
second_request.append(
rh::IF_MODIFIED_SINCE,
rh::HeaderValue::from_static(DATE_ZERO),
);
c.client = rmt::FakeClient::new(
url.clone(),
second_request,
rmt::FakeResponse {
status: reqwest::StatusCode::NOT_MODIFIED,
headers: response_headers,
body: io::Cursor::new(b""[..].into()),
},
);
let mut res = c.get(url).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, body);
c.client.assert_called();
}
#[test]
fn update_cache_if_modified_since() {
let _ = env_logger::try_init();
let url: reqwest::Url = "http://example.com/".parse().unwrap();
let request_1_headers = rh::HeaderMap::new();
let mut response_1_headers = rh::HeaderMap::new();
response_1_headers
.append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ZERO));
let mut c = make_test_cache(rmt::FakeClient::new(
url.clone(),
request_1_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_1_headers,
body: io::Cursor::new(b"hello".as_ref().into()),
},
));
c.get(url.clone()).unwrap();
c.client.assert_called();
let mut request_2_headers = rh::HeaderMap::new();
request_2_headers.append(
rh::IF_MODIFIED_SINCE,
rh::HeaderValue::from_static(DATE_ZERO),
);
let mut response_2_headers = rh::HeaderMap::new();
response_2_headers
.append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ONE));
c.client = rmt::FakeClient::new(
url.clone(),
request_2_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_2_headers,
body: io::Cursor::new(b"world".as_ref().into()),
},
);
let mut res = c.get(url.clone()).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"world");
c.client.assert_called();
let mut request_3_headers = rh::HeaderMap::new();
request_3_headers.append(
rh::IF_MODIFIED_SINCE,
rh::HeaderValue::from_static(DATE_ONE),
);
let response_3_headers = rh::HeaderMap::new();
c.client = rmt::FakeClient::new(
url.clone(),
request_3_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::NOT_MODIFIED,
headers: response_3_headers,
body: io::Cursor::new(b"".as_ref().into()),
},
);
let mut res = c.get(url).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"world");
c.client.assert_called();
}
#[test]
fn return_existing_data_on_connection_refused() {
let _ = env_logger::try_init();
let temp_path = tempdir::TempDir::new("http-cache-test")
.unwrap()
.into_path();
let url: reqwest::Url = "http://example.com/".parse().unwrap();
let request_1_headers = rh::HeaderMap::new();
let mut response_1_headers = rh::HeaderMap::new();
response_1_headers
.append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ZERO));
let mut c = super::Cache::new(
temp_path.clone(),
rmt::FakeClient::new(
url.clone(),
request_1_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_1_headers,
body: io::Cursor::new(b"hello".as_ref().into()),
},
),
)
.unwrap();
c.get(url.clone()).unwrap();
c.client.assert_called();
let mut request_2_headers = rh::HeaderMap::new();
request_2_headers.append(
rh::IF_MODIFIED_SINCE,
rh::HeaderValue::from_static(DATE_ZERO),
);
let mut c = super::Cache::new(
temp_path,
rmt::BrokenClient::new(url.clone(), request_2_headers, || {
rmt::FakeError.into()
}),
)
.unwrap();
let mut res = c.get(url).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"hello");
c.client.assert_called();
}
#[test]
fn use_cache_data_if_some_match() {
let _ = env_logger::try_init();
let url: reqwest::Url = "http://example.com/".parse().unwrap();
let body = b"hello world";
let mut response_headers = rh::HeaderMap::new();
response_headers.append(rh::ETAG, rh::HeaderValue::from_static("abcd"));
let mut c = make_test_cache(rmt::FakeClient::new(
url.clone(),
rh::HeaderMap::new(),
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_headers.clone(),
body: io::Cursor::new(body.as_ref().into()),
},
));
c.get(url.clone()).unwrap();
c.client.assert_called();
let mut second_request = rh::HeaderMap::new();
second_request
.append(rh::IF_NONE_MATCH, rh::HeaderValue::from_static("abcd"));
c.client = rmt::FakeClient::new(
url.clone(),
second_request,
rmt::FakeResponse {
status: reqwest::StatusCode::NOT_MODIFIED,
headers: response_headers,
body: io::Cursor::new(b""[..].into()),
},
);
let mut res = c.get(url).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, body);
c.client.assert_called();
}
#[test]
fn update_cache_if_none_match() {
let _ = env_logger::try_init();
let url: reqwest::Url = "http://example.com/".parse().unwrap();
let request_1_headers = rh::HeaderMap::new();
let mut response_1_headers = rh::HeaderMap::new();
response_1_headers
.append(rh::ETAG, rh::HeaderValue::from_static("abcd"));
let mut c = make_test_cache(rmt::FakeClient::new(
url.clone(),
request_1_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_1_headers,
body: io::Cursor::new(b"hello".as_ref().into()),
},
));
c.get(url.clone()).unwrap();
c.client.assert_called();
let mut request_2_headers = rh::HeaderMap::new();
request_2_headers
.append(rh::IF_NONE_MATCH, rh::HeaderValue::from_static("abcd"));
let mut response_2_headers = rh::HeaderMap::new();
response_2_headers
.append(rh::ETAG, rh::HeaderValue::from_static("efgh"));
c.client = rmt::FakeClient::new(
url.clone(),
request_2_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::OK,
headers: response_2_headers,
body: io::Cursor::new(b"world".as_ref().into()),
},
);
let mut res = c.get(url.clone()).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"world");
c.client.assert_called();
let mut request_3_headers = rh::HeaderMap::new();
request_3_headers
.append(rh::IF_NONE_MATCH, rh::HeaderValue::from_static("efgh"));
let response_3_headers = rh::HeaderMap::new();
c.client = rmt::FakeClient::new(
url.clone(),
request_3_headers,
rmt::FakeResponse {
status: reqwest::StatusCode::NOT_MODIFIED,
headers: response_3_headers,
body: io::Cursor::new(b"".as_ref().into()),
},
);
let mut res = c.get(url).unwrap();
let mut buf = vec![];
res.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"world");
c.client.assert_called();
}
}