1#![warn(
2 missing_copy_implementations,
4 missing_debug_implementations,
5 missing_docs,
6 unreachable_pub,
7
8 clippy::clone_on_ref_ptr,
10 clippy::dbg_macro,
11 clippy::decimal_literal_representation,
12 clippy::float_cmp_const,
13 clippy::get_unwrap,
14 clippy::integer_arithmetic,
15 clippy::integer_division,
16 clippy::pedantic,
17 clippy::print_stdout,
18)]
19
20use std::{
26 ffi::OsStr,
27 fmt,
28 fs::File,
29 io::Read,
30 path::{Path, PathBuf},
31 time::Duration,
32};
33
34use anyhow::{anyhow, Error};
35use flate2::read::GzDecoder;
36use ureq::{Agent, Response};
37use url::Url;
38
39#[derive(Debug)]
42pub struct Fetcher {
43 pub client: Agent,
45}
46impl Default for Fetcher {
47 fn default() -> Self {
48 Self {
49 client: ureq::agent().build(),
50 }
51 }
52}
53impl Fetcher {
54 pub fn new() -> Self {
56 Self::default()
57 }
58 pub fn open<F: Fetchable>(&mut self, resource: F) -> Result<F::Reader, F::Error> {
60 resource.reader_for(self)
61 }
62}
63
64pub trait Fetchable {
69 type Reader: Read;
71 type Error;
73
74 fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error>;
76}
77
78impl Fetchable for &str {
79 type Reader = Box<dyn Read>;
80 type Error = Error;
81
82 fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
83 match Url::parse(self) {
84 Ok(path) => path.reader_for(f),
85 Err(_) => Path::new(self).reader_for(f),
86 }
87 }
88}
89
90impl Fetchable for Url {
91 type Reader = Box<dyn Read>;
92 type Error = Error;
93
94 fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
95 match self.to_file_path() {
96 Ok(path) => path.reader_for(f),
97 Err(()) => f
98 .client
99 .get(self.as_str())
100 .timeout(Duration::from_secs(3))
101 .call()
102 .reader_for(f),
103 }
104 }
105}
106
107impl Fetchable for Response {
108 type Reader = Box<dyn Read>;
109 type Error = Error;
110
111 fn reader_for(self, _f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
112 if self.header("Content-Type").map_or(false, |v| v == "application/x-gzip")
113 || self
114 .header("Content-Disposition")
115 .map_or(false, |v| v.contains(".gz"))
116 {
117 Ok(Box::new(GzDecoder::new(self.into_reader())))
118 } else {
119 Ok(Box::new(self.into_reader()))
120 }
121 }
122}
123
124impl Fetchable for &Path {
125 type Reader = Box<dyn Read>;
126 type Error = Error;
127
128 fn reader_for(self, _f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
129 let file = File::open(self)?;
130
131 if self.extension().map_or(false, |ext| ext == "gz") {
132 Ok(Box::new(GzDecoder::new(file)))
133 } else {
134 Ok(Box::new(file))
135 }
136 }
137}
138impl Fetchable for PathBuf {
139 type Reader = Box<dyn Read>;
140 type Error = Error;
141
142 fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
143 (&*self).reader_for(f)
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
150pub enum Resource {
151 Url(Url),
154 PathBuf(PathBuf),
156}
157impl Fetchable for Resource {
158 type Reader = Box<dyn Read>;
159 type Error = Error;
160
161 fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
162 match self {
163 Self::Url(url) => url.reader_for(f),
164 Self::PathBuf(path) => path.reader_for(f),
165 }
166 }
167}
168impl Resource {
169 pub fn is_absolute(&self) -> bool {
172 match self {
173 Self::Url(_) => true,
174 Self::PathBuf(path) => path.is_absolute(),
175 }
176 }
177
178 pub fn join(mut self, other: Self) -> Result<Self, Error> {
181 if other.is_absolute() {
182 return Ok(other);
183 }
184
185 let other_str = other.as_ref().to_str().ok_or(anyhow!("UTF-8 error"))?;
186
187 match self {
188 Self::Url(parent) => parent.join(other_str).map(Self::Url).map_err(Error::from),
189 Self::PathBuf(ref mut parent) => {
190 parent.set_file_name(other_str);
191 Ok(self)
192 }
193 }
194 }
195}
196impl From<PathBuf> for Resource {
197 fn from(path: PathBuf) -> Self {
198 Self::PathBuf(path)
199 }
200}
201impl From<Url> for Resource {
202 fn from(url: Url) -> Self {
203 Self::Url(url)
204 }
205}
206impl From<String> for Resource {
207 fn from(s: String) -> Self {
208 match Url::parse(&s) {
209 Ok(path) => Self::Url(path),
210 Err(_) => Self::PathBuf(PathBuf::from(s)),
211 }
212 }
213}
214impl From<&str> for Resource {
215 fn from(s: &str) -> Self {
216 match Url::parse(s) {
217 Ok(path) => Self::Url(path),
218 Err(_) => Self::PathBuf(PathBuf::from(s)),
219 }
220 }
221}
222impl AsRef<OsStr> for Resource {
223 fn as_ref(&self) -> &OsStr {
224 match self {
225 Self::Url(url) => OsStr::new(url.as_str()),
226 Self::PathBuf(path) => OsStr::new(path),
227 }
228 }
229}
230impl fmt::Display for Resource {
231 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
232 match self {
233 Self::Url(url) => url.fmt(f),
234 Self::PathBuf(path) => path.display().fmt(f),
235 }
236 }
237}
238
239#[cfg(feature = "serde")]
240impl serde::Serialize for Resource {
241 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
242 match self {
243 Self::Url(url) => serializer.serialize_str(url.as_str()),
244 Self::PathBuf(path) => path.serialize(serializer),
245 }
246 }
247}
248
249#[cfg(feature = "serde")]
250impl<'de> serde::Deserialize<'de> for Resource {
251 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
252 <&str>::deserialize(deserializer).map(Self::from)
253 }
254}