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