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