Skip to main content

rust_releases_io/client/
cached_client.rs

1use crate::client::errors::{HttpError, IoError};
2use crate::client::remote_client::HttpClient;
3use crate::{
4    is_stale, ClientError, Document, IsStaleError, ResourceFile, RetrievalLocation,
5    RetrievedDocument, RustReleasesClient,
6};
7use std::fs;
8use std::io::{self, BufReader, BufWriter, Read, Write};
9use std::path::{Path, PathBuf};
10use std::time::Duration;
11
12const DEFAULT_MEMORY_SIZE: usize = 4096;
13
14const DEFAULT_TIMEOUT: Duration = Duration::from_secs(150);
15
16/// The client to download and cache rust releases.
17///
18/// If a cached file is not present, or if a cached file is present, but the copy is outdated,
19/// the client will download a new copy of the given resource and store it to the `cache_folder`.
20/// If a cached file is present, and the copy is not outdated, the cached file will be returned
21/// instead.
22#[derive(Debug)]
23pub struct HttpCachedClient {
24    cache_folder: PathBuf,
25    cache_timeout: Duration,
26}
27
28impl HttpCachedClient {
29    /// Create a new [`HttpCachedClient`].
30    ///
31    /// ```
32    /// use std::time::Duration;
33    /// use rust_releases_io::{base_cache_dir, HttpCachedClient};
34    /// let cache_folder = base_cache_dir().unwrap();
35    /// let timeout = Duration::from_secs(86_400);
36    ///
37    /// let _client = HttpCachedClient::new(cache_folder, timeout);
38    /// ```
39    pub fn new(cache_folder: PathBuf, cache_timeout: Duration) -> Self {
40        Self {
41            cache_folder,
42            cache_timeout,
43        }
44    }
45}
46
47impl RustReleasesClient for HttpCachedClient {
48    type Error = HttpCachedClientError;
49
50    fn fetch(&self, resource: ResourceFile) -> Result<RetrievedDocument, Self::Error> {
51        let path = self.cache_folder.join(resource.name());
52        let exists = path.exists();
53
54        // Returned the cached document if it exists and is not stale
55        if exists && !is_stale(&path, self.cache_timeout)? {
56            let buffer = read_from_path(&path)?;
57            let document = Document::new(buffer);
58
59            return Ok(RetrievedDocument::new(
60                document,
61                RetrievalLocation::Path(path),
62            ));
63        }
64
65        // Ensure we have a place to put the cached document.
66        if !exists {
67            setup_cache_folder(&path)?;
68        }
69
70        let client = HttpClient::new(DEFAULT_TIMEOUT);
71        let mut retrieved = client
72            .fetch(resource)
73            .map_err(HttpCachedClientError::from)?;
74
75        let document = retrieved.mut_document();
76
77        // write to memory
78        write_document_and_cache(document, &path)?;
79
80        Ok(retrieved)
81    }
82}
83
84fn read_from_path(path: &Path) -> Result<Vec<u8>, HttpCachedClientError> {
85    let mut reader = BufReader::new(
86        fs::File::open(path).map_err(|err| IoError::inaccessible(err, path.to_path_buf()))?,
87    );
88
89    let mut memory = Vec::with_capacity(DEFAULT_MEMORY_SIZE);
90    reader
91        .read_to_end(&mut memory)
92        .map_err(IoError::auxiliary)?;
93
94    Ok(memory)
95}
96
97/// `manifest_path` should include the cache folder and name of the manifest file.
98fn setup_cache_folder(manifest_path: &Path) -> Result<(), HttpCachedClientError> {
99    fn create_dir_all(path: &Path) -> Result<(), IoError> {
100        fs::create_dir_all(path).map_err(|err| IoError::inaccessible(err, path.to_path_buf()))
101    }
102
103    // Check we're not at the root of the file system.
104    if let Some(cache_folder) = manifest_path.parent() {
105        // Check that the cache folder doesn't exist yet.
106        match fs::metadata(cache_folder) {
107            // If the folder already exists we don't need to do anything.
108            Ok(m) if m.is_dir() => Ok(()),
109            // A file with the same name exists. In the common tree based filesystem where only directories
110            // can hold files, this should never happen, since we're already in the `manifest_path.parent()`
111            // call.
112            Ok(_) => Err(IoError::is_file(cache_folder.to_path_buf())),
113            // If the folder is not found, we create it.
114            Err(err) if err.kind() == io::ErrorKind::NotFound => create_dir_all(cache_folder),
115            // If the folder
116            Err(err) => Err(IoError::inaccessible(err, cache_folder.to_path_buf())),
117        }?;
118    }
119
120    Ok(())
121}
122
123fn write_document_and_cache(
124    document: &mut Document,
125    file_path: &Path,
126) -> Result<(), HttpCachedClientError> {
127    let mut file = fs::File::create(file_path)
128        .map_err(|err| IoError::inaccessible(err, file_path.to_path_buf()))?;
129
130    let mut writer = BufWriter::new(&mut file);
131    writer
132        .write_all(document.buffer())
133        .map_err(|err| IoError::inaccessible(err, file_path.to_path_buf()))?;
134
135    Ok(())
136}
137
138/// A list of errors which may be produced by [`HttpCachedClient::fetch`].
139#[derive(Debug, thiserror::Error)]
140#[non_exhaustive]
141pub enum HttpCachedClientError {
142    /// Returned if the fetched file was empty.
143    #[error("Received empty file")]
144    EmptyFile,
145
146    /// Returned if the HTTP client could not fetch an item
147    #[error(transparent)]
148    Http(#[from] HttpError),
149
150    /// Returned in case of an `std::io::Error`.
151    #[error(transparent)]
152    Io(#[from] IoError),
153
154    /// Returned in case it wasn't possible to check whether the cache file is
155    /// stale or not.
156    #[error(transparent)]
157    IsStale(#[from] IsStaleError),
158}
159
160impl From<ClientError> for HttpCachedClientError {
161    fn from(err: ClientError) -> Self {
162        match err {
163            ClientError::Empty => HttpCachedClientError::EmptyFile,
164            ClientError::Http(err) => HttpCachedClientError::Http(err),
165            ClientError::Io(err) => HttpCachedClientError::Io(err),
166        }
167    }
168}