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#[async_trait]
17pub trait Cache {
18 type Read: AsyncBufRead + Unpin;
20 type Write: AsyncWrite + Unpin;
22
23 async fn get(&self, url: &Url) -> anyhow::Result<Option<Self::Read>>;
25
26 async fn insert(&self, url: &Url) -> anyhow::Result<Self::Write>;
28}
29
30pub 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 pub fn into_inner(self) -> T {
70 self.0
71 }
72}
73
74#[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 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}