cache.rs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. // use sha256;
  2. use std::fs::{File, create_dir_all, read_dir, remove_file};
  3. use std::io::{BufRead, BufReader, Write};
  4. use std::path::PathBuf;
  5. use std::result::Result;
  6. use std::time::{Duration, SystemTime};
  7. use url::Url;
  8. // Error
  9. use std::error::Error as Errorr;
  10. use std::fmt;
  11. #[deny(missing_docs)]
  12. // #[warn(missing_docs)]
  13. /// Convert relate to absolute
  14. ///
  15. /// This can fail if Url is unable to parse, or Url is unable to join.
  16. #[must_use]
  17. pub fn relative_to_absolute(
  18. base_url: &str,
  19. relative_href: &str,
  20. ) -> Result<String, url::ParseError> {
  21. let base_url = Url::parse(base_url)?;
  22. let new_url = base_url.join(relative_href)?;
  23. Ok(new_url.to_string())
  24. }
  25. /// Save reqwest::header::HeaderMap to file.
  26. ///
  27. /// This also stores the url in the file, so I know what URL was called for
  28. /// this reqwest.
  29. ///
  30. /// It has each item on a single line:
  31. /// header: value
  32. /// The first line will be url: (Which is not part of original header.)
  33. pub fn save_headermap(
  34. filename: &str,
  35. url: &str,
  36. header: &reqwest::header::HeaderMap,
  37. ) -> Result<(), std::io::Error> {
  38. let mut fp = File::create(filename)?;
  39. fp.write_all(format!("url: {}\n", url).as_bytes())?;
  40. for (key, value) in header.iter() {
  41. if let Ok(value) = value.to_str() {
  42. fp.write_all(format!("{}: {}\n", key, value).as_bytes())?;
  43. }
  44. }
  45. Ok(())
  46. }
  47. /// Load reqwest::header::HeaderMap from file.
  48. ///
  49. /// This will have the url of the original call in the "url" section.
  50. pub fn load_headermap(filename: &str) -> Result<reqwest::header::HeaderMap, std::io::Error> {
  51. let fp = File::open(filename)?;
  52. let mut buffer = BufReader::new(fp);
  53. let mut line = String::new();
  54. let mut header = reqwest::header::HeaderMap::new();
  55. loop {
  56. if buffer.read_line(&mut line).unwrap() == 0 {
  57. break;
  58. };
  59. let temp = line.trim_end();
  60. if let Some(parts) = temp.split_once(": ") {
  61. let head = reqwest::header::HeaderName::from_bytes(parts.0.as_bytes()).unwrap();
  62. if let Ok(value) = reqwest::header::HeaderValue::from_str(&parts.1) {
  63. header.insert(head, value);
  64. }
  65. }
  66. line.clear();
  67. }
  68. Ok(header)
  69. }
  70. /// Caching web calls
  71. ///
  72. /// Set the directory, and we're ready to make cached web calls.
  73. /// Since we're not storing the file in memory now, max_size isn't
  74. /// the concern it once was.
  75. pub struct Cache {
  76. /// Directory where cache is stored
  77. pub directory: PathBuf,
  78. // *This is where we would select async or blocking.*
  79. /// Reqwest Client
  80. pub client: reqwest::blocking::Client,
  81. /// Vector of content-types to accept (empty=all)
  82. pub accept: Vec<String>,
  83. /// Max size of content to download (default unlimited)
  84. pub max_size: Option<u64>,
  85. }
  86. // Should I also have std::io::Errors in here as well?
  87. // I can have File IO errors.
  88. /// Status of fetch
  89. #[allow(dead_code)]
  90. #[derive(Debug)]
  91. #[repr(u8)]
  92. pub enum Status {
  93. /// File was downloaded.
  94. Fetched(PathBuf),
  95. /// File was retrieved from cache.
  96. Cached(PathBuf),
  97. }
  98. impl Status {
  99. /// Return pathbuf, always
  100. pub fn download_path(&self) -> &PathBuf {
  101. match self {
  102. Status::Fetched(path) | Status::Cached(path) => {
  103. return path;
  104. }
  105. }
  106. }
  107. }
  108. #[derive(Debug)]
  109. pub enum Error {
  110. /// Reqwest error (unable to connect), or IO Error std::io::Error
  111. ReqwestError(reqwest::Error),
  112. IOError(std::io::Error),
  113. /// Content-Type wasn't allowed, see Cache.accept.
  114. Unacceptable(String), // Content-Type
  115. /// Content was too big, see Cache.max_size.
  116. TooBig(u64),
  117. /// HTTP Error/status code.
  118. HttpErrorStatus(u16),
  119. }
  120. // This allows ? to return cache::Error from std::io::Error (see expire)
  121. impl From<std::io::Error> for Error {
  122. fn from(e: std::io::Error) -> Self {
  123. Self::IOError(e)
  124. }
  125. }
  126. impl From<reqwest::Error> for Error {
  127. fn from(e: reqwest::Error) -> Self {
  128. Self::ReqwestError(e)
  129. }
  130. }
  131. impl fmt::Display for Error {
  132. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  133. match self {
  134. Error::ReqwestError(e) => write!(f, "ReqwestError: {:?}", e),
  135. Error::IOError(e) => write!(f, "IOError: {:?}", e),
  136. Error::Unacceptable(ct) => write!(f, "Content-Type {} not allowed", ct),
  137. Error::TooBig(size) => write!(f, "Content-Size {} too big", size),
  138. Error::HttpErrorStatus(status) => write!(f, "Status Code: {}", status),
  139. }
  140. }
  141. }
  142. // This made anyhow happy with my cache::Error.
  143. impl Errorr for Error {}
  144. /*
  145. Some possible content-type values: We're only interested in a few of these...
  146. text/css
  147. text/javascript
  148. text/plain
  149. image/jpeg
  150. image/png
  151. iamge/gif
  152. application/xml
  153. application/javascript
  154. */
  155. // If nothing is given for useragent, we default to the application name and version.
  156. static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
  157. static HEADER_EXT: &str = ".header";
  158. impl Cache {
  159. /// Construct Cache using given directory for caching, and useragent.
  160. pub fn new(dir: PathBuf, useragent: Option<&str>) -> Result<Self, Error> {
  161. // Verify the directory exists
  162. let path = dir.as_path();
  163. if path.exists() {
  164. if !path.is_dir() {
  165. // It exists, but it isn't a directory! What?!
  166. return Err(Error::IOError(std::io::Error::new(
  167. std::io::ErrorKind::Other,
  168. format!(
  169. "Can't create Cache dir {}, it already exists.",
  170. dir.display()
  171. ),
  172. )));
  173. }
  174. } else {
  175. match create_dir_all(path) {
  176. Err(e) => {
  177. return Err(Error::IOError(e));
  178. }
  179. Ok(_) => {}
  180. }
  181. }
  182. let user_agent = if let Some(ua) = useragent {
  183. ua
  184. } else {
  185. APP_USER_AGENT
  186. };
  187. // This is where we select async or blocking.
  188. match reqwest::blocking::Client::builder()
  189. .user_agent(user_agent)
  190. .build()
  191. {
  192. Ok(client) => {
  193. Ok(Self {
  194. directory: dir,
  195. client: client,
  196. accept: vec![], // Accept all content-type.
  197. max_size: None, // Some(256 * 1024 * 1024), // 256 MB
  198. })
  199. }
  200. Err(e) => {
  201. // Client::builder error
  202. return Err(Error::ReqwestError(e));
  203. }
  204. }
  205. }
  206. #[allow(dead_code)]
  207. pub fn add_content_type(&mut self, content_type: String) {
  208. self.accept.push(content_type);
  209. }
  210. #[allow(dead_code)]
  211. pub fn clear_content_type(&mut self) {
  212. self.accept.clear();
  213. }
  214. #[allow(dead_code)]
  215. pub fn set_max_size(&mut self, size: u64) {
  216. self.max_size = Some(size);
  217. }
  218. #[allow(dead_code)]
  219. pub fn clear_max_size(&mut self) {
  220. self.max_size = None;
  221. }
  222. /// Create safe filename from url for header/content files.
  223. pub fn url_to_basename(url: &str) -> String {
  224. let filename = if url.ends_with("/") {
  225. ""
  226. } else {
  227. if let Some(has_file) = url.rsplit_once("/") {
  228. has_file.1
  229. } else {
  230. ""
  231. }
  232. };
  233. if filename.is_empty() {
  234. // Getting the filename part failed.
  235. // Like in cases where the url is https://go.dev/dl/
  236. // Which becomes go.dev-dl
  237. let mut path = url.to_string();
  238. path = path.replace("https://", "");
  239. path = path.replace("http://", "");
  240. path = path.replace("/", "-");
  241. path = path.replace(".", "-");
  242. if path.ends_with("-") {
  243. path.pop();
  244. }
  245. return path;
  246. }
  247. filename.to_string()
  248. }
  249. /// Expire files in the cache older then given age
  250. ///
  251. /// Use DirEntry.modified, since it updates when a file is freshened/downloaded.
  252. /// DirEntry.created isn't updated when file is rewritten.
  253. #[allow(dead_code)]
  254. pub fn expire(&self, age: Duration) -> Result<bool, Error> {
  255. let now = SystemTime::now();
  256. let mut result: bool = false;
  257. for file in read_dir(self.directory.as_path())? {
  258. let file = file?;
  259. if let Ok(d) = file.metadata() {
  260. if d.is_file() {
  261. // Created isn't updated if the file is fetched. Use modified, that updates on fetch.
  262. let filename = String::from(file.file_name().to_str().unwrap());
  263. if filename.ends_with(HEADER_EXT) {
  264. // This is a header cache file...
  265. if let Ok(modify) = d.modified() {
  266. if let Ok(delta) = now.duration_since(modify) {
  267. // println!("expire {} = modified {}", filename, delta.as_secs());
  268. if delta > age {
  269. // println!("Would delete: {} (and .content)", filename);
  270. let mut filepath = self.directory.join(filename);
  271. let r = remove_file(&filepath);
  272. if let Err(e) = r {
  273. println!("RemoveFile {:?}: {}", filepath, e);
  274. }
  275. // Also delete .content !
  276. // Which is trickier to find now...
  277. Self::remove_from_filename(&mut filepath);
  278. // filepath.set_extension("content");
  279. let r = remove_file(&filepath);
  280. if let Err(e) = r {
  281. println!("RemoveFile {:?}: {}", filepath, e);
  282. }
  283. result = true;
  284. }
  285. }
  286. }
  287. }
  288. /*
  289. if let Ok(access) = d.accessed() {
  290. if let Ok(delta) = now.duration_since(access) {
  291. println!("accessed {:?} = accessed {}", file.file_name(), delta.as_secs());
  292. if delta > age {
  293. println!("Expire: {:?}", file.file_name());
  294. }
  295. }
  296. }
  297. if let Ok(created) = d.created() {
  298. if let Ok(delta) = now.duration_since(created) {
  299. println!("expire {:?} = created {}", file.file_name(), delta.as_secs());
  300. if delta > age {
  301. println!("Would delete: {:?}", file.file_name());
  302. result = true;
  303. }
  304. }
  305. }
  306. */
  307. }
  308. }
  309. }
  310. Ok(result)
  311. }
  312. /// Given a url, return the filename
  313. ///
  314. /// The filename might not exist. It is only the filename
  315. /// that would be used for the given url.
  316. pub fn filename_for_url(&self, url: &str) -> PathBuf {
  317. self.directory.as_path().join(Self::url_to_basename(url))
  318. }
  319. /// Given a url, return an open file
  320. ///
  321. /// Reading from cache.
  322. #[allow(dead_code)]
  323. pub fn file(&self, url: &str) -> Option<File> {
  324. let base = self.filename_for_url(url);
  325. /*
  326. let base = self
  327. .directory
  328. .as_path()
  329. .join(Self::url_to_basename(url).unwrap());
  330. */
  331. if base.exists() {
  332. return Some(File::open(base).unwrap());
  333. }
  334. None
  335. }
  336. /// Return filename from pathbuf as String
  337. #[allow(dead_code)]
  338. #[must_use]
  339. fn pathbuf_filename(path: &PathBuf) -> String {
  340. path.file_name().unwrap().to_string_lossy().to_string()
  341. }
  342. /// Add to the PathBuf filename
  343. ///
  344. /// This is different then PathBuf::set_extension
  345. /// which replaces everything.
  346. fn append_to_filename(path: &mut PathBuf, append: &str) {
  347. // Append to the filename.
  348. let filename = path.file_name().unwrap().to_string_lossy().to_string() + append;
  349. path.set_file_name(filename);
  350. }
  351. /// Remove an extension from the filename.
  352. ///
  353. /// Given something.tar.gz.header return something.tar.gz
  354. fn remove_from_filename(path: &mut PathBuf) {
  355. let filename = Self::pathbuf_filename(path);
  356. if let Some(parts) = filename.rsplit_once(".") {
  357. path.set_file_name(parts.0);
  358. } else {
  359. panic!(
  360. "Unable to locate the trailing extension . from: {}",
  361. path.display()
  362. );
  363. }
  364. }
  365. /// Fetch, without using the cache.
  366. ///
  367. /// This deletes the .header cache file, which forces a fetch.
  368. #[allow(dead_code)]
  369. pub fn fetch_nocache(&self, url: &str) -> Result<Status, Error> {
  370. let mut base = self.filename_for_url(url);
  371. Self::append_to_filename(&mut base, HEADER_EXT);
  372. if base.exists() {
  373. match remove_file(&base) {
  374. Err(e) => {
  375. // unlink failed
  376. return Err(Error::IOError(e));
  377. }
  378. Ok(_) => {}
  379. }
  380. }
  381. return self.fetch(url);
  382. }
  383. // I'm not sure about using Result<Status> here...
  384. // It would allow for ? usage.
  385. /// Fetch the URL from the web
  386. ///
  387. /// This returns Status, which could be Fetched or Cached copy (among other things).
  388. #[must_use]
  389. pub fn fetch(&self, url: &str) -> Result<Status, Error> {
  390. let base = self.filename_for_url(url);
  391. /*
  392. let base = self
  393. .directory
  394. .as_path()
  395. .join(Self::url_to_basename(url).unwrap());
  396. */
  397. let mut builder = self.client.get(url);
  398. // Don't send just yet!
  399. // Set some headers to see if page content has changed.
  400. let mut header_file = base.clone();
  401. Self::append_to_filename(&mut header_file, HEADER_EXT);
  402. if header_file.exists() {
  403. // Ok! We have existing information. Retrieve it.
  404. match load_headermap(header_file.to_str().unwrap()) {
  405. Ok(old_header) => {
  406. // Look for: ETag, Last-Modified
  407. if let Some(lastmod) = old_header.get("Last-Modified") {
  408. builder = builder.header("If-Modified-Since", lastmod);
  409. } else if let Some(date) = old_header.get("Date") {
  410. // Keep trying...
  411. builder = builder.header("If-Modified-Since", date);
  412. }
  413. if let Some(etag) = old_header.get("etag") {
  414. builder = builder.header("If-None-Match", etag);
  415. }
  416. }
  417. Err(e) => {
  418. return Err(Error::IOError(e));
  419. }
  420. }
  421. };
  422. match builder.send() {
  423. Ok(mut result) => {
  424. if result.status() == 304 {
  425. // Cache hit!
  426. return Ok(Status::Cached(base));
  427. }
  428. // Ok! Success!
  429. if result.status() == 200 {
  430. // Success!
  431. // When caching fails ―
  432. //
  433. // If content_length (from previous fetch) matches current?
  434. // Could we assume it hasn't changed, and just use cache?
  435. // Or would that be a big assumption?
  436. // Only check content_length size, if we have been
  437. // given a max_size.
  438. if let Some(max_size) = self.max_size {
  439. if let Some(len) = result.content_length() {
  440. if len > max_size {
  441. // Is there a way to abort this safely? Apparently yes! :D
  442. // let byte = Byte::from_u64(len);
  443. // let adjusted_byte = byte.get_appropriate_unit(UnitType::Binary);
  444. // println!("Too Big! {adjusted_byte:.2} {}", url);
  445. return Err(Error::TooBig(len));
  446. }
  447. }
  448. }
  449. // Only check acceptable content_types if given.
  450. if !self.accept.is_empty() {
  451. if let Some(content_type) = result.headers().get("content-type") {
  452. // Check to see if accepted content.
  453. let mut ct = content_type.to_str().unwrap();
  454. let possible = content_type.to_str().unwrap().split_once(';');
  455. if let Some((ct_part, _)) = possible {
  456. ct = ct_part;
  457. }
  458. if !self.accept.contains(&ct.to_string()) {
  459. // println!("Unacceptable content-type {} {}", ct, url);
  460. return Err(Error::Unacceptable(ct.to_string()));
  461. }
  462. }
  463. }
  464. match save_headermap(header_file.to_str().unwrap(), url, result.headers()) {
  465. Err(e) => {
  466. return Err(Error::IOError(e));
  467. }
  468. Ok(()) => {}
  469. }
  470. match File::create(base.to_str().unwrap()) {
  471. Ok(mut fp) => match result.copy_to(&mut fp) {
  472. Ok(_) => {}
  473. Err(e) => {
  474. return Err(Error::ReqwestError(e));
  475. }
  476. },
  477. Err(e) => {
  478. return Err(Error::IOError(e));
  479. }
  480. }
  481. // result.copy_to(&mut fp)?;
  482. /* // async
  483. while let Ok(Some(chunk)) = result.chunk().await {
  484. let _ = fp.write(&chunk);
  485. }
  486. */
  487. return Ok(Status::Fetched(base));
  488. } else {
  489. // Status error
  490. // println!("Error {} {}", result.status(), url);
  491. return Err(Error::HttpErrorStatus(u16::from(result.status())));
  492. }
  493. }
  494. Err(e) => {
  495. return Err(Error::ReqwestError(e));
  496. }
  497. }
  498. }
  499. }
  500. /*
  501. https://httpbin.org/anything
  502. /headers
  503. /ip
  504. /user-agent
  505. /status/404
  506. /status/200
  507. /cache/value for cache-control
  508. /cache (if-modified-since or if-none-match are present, returns 304)
  509. /etag/value for etag (if-none-match or if-match)
  510. /uuid
  511. /brotli
  512. /deflate
  513. /gzip
  514. ^ I wonder what happens if I request one that isn't enabled in reqwest?
  515. */
  516. #[cfg(test)]
  517. mod tests {
  518. use super::*;
  519. use std::collections::HashMap;
  520. use testdir::testdir;
  521. #[test]
  522. fn relative_test() {
  523. let rel_abs: HashMap<(&str, &str), &str> = HashMap::from([
  524. (
  525. ("http://meow.org/rabbit", "/llama/index.html"),
  526. "http://meow.org/llama/index.html",
  527. ),
  528. (
  529. ("https://example.com/dir/index.html", "about.html"),
  530. "https://example.com/dir/about.html",
  531. ),
  532. (
  533. ("https://example.com/dir/index.html", "../and/about.html"),
  534. "https://example.com/and/about.html",
  535. ),
  536. (
  537. (
  538. "https://here.com/dir/index.html",
  539. "http://there.com/about.html",
  540. ),
  541. "http://there.com/about.html",
  542. ),
  543. ]);
  544. for (base, url) in rel_abs {
  545. if let Ok(abs) = relative_to_absolute(base.0, base.1) {
  546. assert_eq!(abs, url, "Base {}, Rel {} => {}", base.0, base.1, url);
  547. } else {
  548. panic!("Failed {} + {} => {}", base.0, base.1, url);
  549. }
  550. }
  551. }
  552. #[test]
  553. fn url_to_filename_test() {
  554. let mut dir = testdir!();
  555. dir.push("cache");
  556. let temp = dir.clone();
  557. let cache = Cache::new(dir, None).unwrap();
  558. // url_to_basename
  559. let url_base: HashMap<&str, &str> = HashMap::from([
  560. ("https://go.dev/dl/go1.23.45.tar.gz", "go1.23.45.tar.gz"),
  561. ("https://go.dev/dl", "dl"),
  562. ("https://go.dev/dl/", "go-dev-dl"),
  563. ]);
  564. for (url, base) in url_base {
  565. // Verify url_to_basename.
  566. let basename = Cache::url_to_basename(url);
  567. assert_eq!(base, basename, "{} -> {}", url, base);
  568. // Verify filename_for_url.
  569. let path = cache.filename_for_url(url);
  570. let mut newpath = temp.clone();
  571. newpath.push(base);
  572. assert_eq!(path.as_os_str(), newpath.as_os_str(), "{} -> {}", url, base);
  573. }
  574. for filename in vec!["go1.23.45.tar.gz", "test.html"] {
  575. let newname = String::from(filename) + HEADER_EXT;
  576. let mut newpath = temp.clone();
  577. newpath.set_file_name(filename);
  578. Cache::append_to_filename(&mut newpath, HEADER_EXT);
  579. assert_eq!(
  580. &newpath.file_name().unwrap().to_string_lossy().to_string(),
  581. &newname,
  582. "{} {}",
  583. filename,
  584. HEADER_EXT
  585. );
  586. // Test to make sure this removes HEADER_EXT from the filename.
  587. Cache::remove_from_filename(&mut newpath);
  588. assert_eq!(
  589. &newpath.file_name().unwrap().to_string_lossy().to_string(),
  590. filename,
  591. "{}",
  592. filename
  593. )
  594. }
  595. }
  596. #[test]
  597. #[cfg(not(feature = "local-httpbin"))]
  598. fn cache_fetch() {
  599. let mut dir = testdir!();
  600. dir.push("cache");
  601. // Make a copy of the cache directory PathBuf for verifying paths.
  602. let mut t = dir.clone();
  603. let cache = Cache::new(dir, None).unwrap();
  604. let r = cache.fetch("https://httpbin.org/anything");
  605. t.push("anything");
  606. if let Ok(r) = r {
  607. if let Status::Fetched(f) = r {
  608. assert!(f.exists(), "Cache file exists.");
  609. assert_eq!(f, t, "Cache path is what we were expecting.");
  610. let mut header_file = t.clone();
  611. Cache::append_to_filename(&mut header_file, HEADER_EXT);
  612. assert!(header_file.exists(), "Cache header file exists.");
  613. t.pop();
  614. t.push("anything.header");
  615. assert_eq!(header_file, t, "Cache header path is what we expected.");
  616. } else {
  617. panic!("Cache Status is not Status::Fetched, is: {:?}", r);
  618. }
  619. } else {
  620. panic!("cache.fetch: {:?}", r);
  621. }
  622. }
  623. /*
  624. Add to Config.toml:
  625. [features]
  626. local-httpbin = []
  627. Use:
  628. #[test]
  629. #[cfg(feature = "local-httpbin")]
  630. And then:
  631. cargo test -F local-httpbin -- --show-output
  632. This runs the local httpbin tests.
  633. */
  634. #[test]
  635. #[cfg(feature = "local-httpbin")]
  636. fn call_local() {
  637. let mut dir = testdir!();
  638. dir.push("cache");
  639. // Make a copy of the cache directory PathBuf for verifying paths.
  640. let mut t = dir.clone();
  641. let cache = Cache::new(dir, None).unwrap();
  642. let teapot_url = "http://127.0.0.1/status/418";
  643. let r = cache.fetch(&teapot_url);
  644. if let Err(e) = r {
  645. if let Error::HttpErrorStatus(code) = e {
  646. assert_eq!(code, 418);
  647. } else {
  648. panic!("Not an ErrorStatus");
  649. }
  650. } else {
  651. panic!("Unexpected error: {r:?}");
  652. }
  653. // println!("{:?}", r);
  654. let r = cache.fetch("http://127.0.0.1:1024");
  655. assert!(r.is_err(), "Confirm connection error");
  656. /*
  657. I disabled brotli in the Client builder.
  658. I get an error below about invalid UTF-8. The httpbin server isn't smart
  659. enough to see I don't support it, and sends it anyway. :(
  660. */
  661. /*
  662. let brot_url = "http://127.0.0.1/brotli";
  663. let r = cache.fetch(brot_url);
  664. println!("Brotli: {:?}", r);
  665. if let Status::Fetched(path) = r {
  666. let data = std::fs::read_to_string(path).unwrap();
  667. println!("DATA:\n{}", data);
  668. }
  669. */
  670. }
  671. /*
  672. These tests require a running local httpbin image.
  673. ```
  674. services:
  675. httpbin:
  676. image: kennethreitz/httpbin
  677. ports:
  678. - "80:80"
  679. ```
  680. */
  681. #[test]
  682. #[cfg(feature = "local-httpbin")]
  683. fn cache_local() {
  684. let mut dir = testdir!();
  685. dir.push("cache");
  686. // Make a copy of the cache directory PathBuf for verifying paths.
  687. let mut t = dir.clone();
  688. let cache = Cache::new(dir, None).unwrap();
  689. let etag_url = "http://127.0.0.1/etag/meow";
  690. let r = cache.fetch(&etag_url);
  691. if let Ok(r) = r {
  692. match r {
  693. Status::Fetched(_) => {}
  694. _ => {
  695. panic!("Expected Status::Fetched on 1st request.");
  696. }
  697. }
  698. } else {
  699. panic!("Unexpected error: {r:?}");
  700. }
  701. // 2nd call, the etag header is set.
  702. let r2 = cache.fetch(&etag_url);
  703. if let Ok(r2) = r2 {
  704. match r2 {
  705. Status::Cached(_) => {}
  706. _ => {
  707. panic!("Expected Status::Cached on 2nd request.");
  708. }
  709. }
  710. } else {
  711. panic!("Unexpected error: {r2:?}");
  712. }
  713. // println!("{:?}\n{:?}", r, r2);
  714. }
  715. }