wit_deps/
cache.rs

1use core::fmt;
2use core::ops::{Deref, DerefMut};
3
4use std::ffi::{OsStr, OsString};
5use std::path::{Path, PathBuf};
6
7use anyhow::{bail, Context as _};
8use async_trait::async_trait;
9use directories::ProjectDirs;
10use tokio::fs::{self, File, OpenOptions};
11use tokio::io::{AsyncBufRead, AsyncWrite, BufReader};
12use url::{Host, Url};
13
14/// Resource caching layer
15#[async_trait]
16pub trait Cache {
17    /// Type returned by the [`Self::get`] method
18    type Read: AsyncBufRead + Unpin;
19    /// Type returned by the [`Self::insert`] method
20    type Write: AsyncWrite + Unpin;
21
22    /// Returns a read handle for the entry from the cache associated with a given URL
23    async fn get(&self, url: &Url) -> anyhow::Result<Option<Self::Read>>;
24
25    /// Returns a write handle for the entry associated with a given URL
26    async fn insert(&self, url: &Url) -> anyhow::Result<Self::Write>;
27}
28
29/// Write-only [Cache] wrapper
30pub struct Write<T>(pub T);
31
32impl<T> From<T> for Write<T> {
33    fn from(cache: T) -> Self {
34        Self(cache)
35    }
36}
37
38impl<T> Deref for Write<T> {
39    type Target = T;
40
41    fn deref(&self) -> &Self::Target {
42        &self.0
43    }
44}
45
46impl<T> DerefMut for Write<T> {
47    fn deref_mut(&mut self) -> &mut Self::Target {
48        &mut self.0
49    }
50}
51
52#[async_trait]
53impl<T: Cache + Sync + Send> Cache for Write<T> {
54    type Read = T::Read;
55    type Write = T::Write;
56
57    async fn get(&self, _: &Url) -> anyhow::Result<Option<Self::Read>> {
58        Ok(None)
59    }
60
61    async fn insert(&self, url: &Url) -> anyhow::Result<Self::Write> {
62        self.0.insert(url).await
63    }
64}
65
66impl<T> Write<T> {
67    /// Extracts the inner [Cache]
68    pub fn into_inner(self) -> T {
69        self.0
70    }
71}
72
73/// Local caching layer
74#[derive(Clone, Debug)]
75pub struct Local(PathBuf);
76
77impl fmt::Display for Local {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.0.display())
80    }
81}
82
83impl Deref for Local {
84    type Target = PathBuf;
85
86    fn deref(&self) -> &Self::Target {
87        &self.0
88    }
89}
90
91impl DerefMut for Local {
92    fn deref_mut(&mut self) -> &mut Self::Target {
93        &mut self.0
94    }
95}
96
97impl Local {
98    /// Returns a [Local] cache located at the default system-specific cache directory if such
99    /// could be determined.
100    pub fn cache_dir() -> Option<Self> {
101        ProjectDirs::from("", "", env!("CARGO_PKG_NAME"))
102            .as_ref()
103            .map(ProjectDirs::cache_dir)
104            .map(Self::from)
105    }
106
107    fn path(&self, url: &Url) -> impl AsRef<Path> {
108        let mut path = self.0.clone();
109        match url.host() {
110            Some(Host::Ipv4(ip)) => {
111                path.push(ip.to_string());
112            }
113            Some(Host::Ipv6(ip)) => {
114                path.push(ip.to_string());
115            }
116            Some(Host::Domain(domain)) => {
117                path.push(domain);
118            }
119            _ => {}
120        }
121        if let Some(segments) = url.path_segments() {
122            for seg in segments {
123                path.push(seg);
124            }
125        }
126        path
127    }
128}
129
130#[async_trait]
131impl Cache for Local {
132    type Read = BufReader<File>;
133    type Write = File;
134
135    async fn get(&self, url: &Url) -> anyhow::Result<Option<Self::Read>> {
136        match File::open(self.path(url)).await {
137            Ok(file) => Ok(Some(BufReader::new(file))),
138            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
139            Err(e) => bail!("failed to lookup `{url}` in cache: {e}"),
140        }
141    }
142
143    async fn insert(&self, url: &Url) -> anyhow::Result<Self::Write> {
144        let path = self.path(url);
145        if let Some(parent) = path.as_ref().parent() {
146            fs::create_dir_all(parent)
147                .await
148                .context("failed to create directory")?;
149        }
150        OpenOptions::new()
151            .create_new(true)
152            .write(true)
153            .open(path)
154            .await
155            .context("failed to open file for writing")
156    }
157}
158
159impl From<PathBuf> for Local {
160    fn from(path: PathBuf) -> Self {
161        Self(path)
162    }
163}
164
165impl From<String> for Local {
166    fn from(path: String) -> Self {
167        Self(path.into())
168    }
169}
170
171impl From<OsString> for Local {
172    fn from(path: OsString) -> Self {
173        Self(path.into())
174    }
175}
176
177impl From<&Path> for Local {
178    fn from(path: &Path) -> Self {
179        Self(path.into())
180    }
181}
182
183impl From<&str> for Local {
184    fn from(path: &str) -> Self {
185        Self(path.into())
186    }
187}
188
189impl From<&OsStr> for Local {
190    fn from(path: &OsStr) -> Self {
191        Self(path.into())
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn local_path() {
201        assert_eq!(
202            Local::from("test")
203                .path(
204                    &"https://example.com/foo/bar.tar.gz"
205                        .parse()
206                        .expect("failed to parse URL")
207                )
208                .as_ref(),
209            Path::new("test")
210                .join("example.com")
211                .join("foo")
212                .join("bar.tar.gz")
213        );
214    }
215}