just_fetch/
lib.rs

1#![warn(
2    // Harden built-in lints
3    missing_copy_implementations,
4    missing_debug_implementations,
5    missing_docs,
6    unreachable_pub,
7
8    // Harden clippy lints
9    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
20//! A library to simply fetch stuff, regardless of whether it's from
21//! the internet, from the filesystem, inside a gzipped archive,
22//! whatever. Used by `scaff` so it can focus on what's important: The
23//! contents of the specified archive.
24
25use 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/// A wrapper around `ureq::Agent` that supports opening any kind
40/// of `Fetchable` resource
41#[derive(Debug)]
42pub struct Fetcher {
43    /// The inner client
44    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    /// Create a new instance
55    pub fn new() -> Self {
56        Self::default()
57    }
58    /// Open any kind of `Fetchable` resource
59    pub fn open<F: Fetchable>(&mut self, resource: F) -> Result<F::Reader, F::Error> {
60        resource.reader_for(self)
61    }
62}
63
64/// Something that can be turned into a reader given a
65/// fetcher. Example is a URL which can be turned into a reader by
66/// sending a GET request, or a file path which can be turned into a
67/// reader by just opening the file.
68pub trait Fetchable {
69    /// The output reader type
70    type Reader: Read;
71    /// Any potential error that could occur
72    type Error;
73
74    /// Return a reader given a fetcher
75    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/// An enum wrapping the two common fetchers `PathBuf` and `Url`. This
148/// is more efficient than using `Box<dyn Fetcher>`.
149#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
150pub enum Resource {
151    /// Fetcher for a URL, either for an online `http(s)://` resource
152    /// or for a `file://`
153    Url(Url),
154    /// Fetcher for a local file path
155    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    /// Return true if a resource is absolute. This is used in `join` to
170    /// disgard the first parent part.
171    pub fn is_absolute(&self) -> bool {
172        match self {
173            Self::Url(_) => true,
174            Self::PathBuf(path) => path.is_absolute(),
175        }
176    }
177
178    /// Join a resource (relative or absolute) with `self`. Useful for
179    /// resolving relative paths.
180    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}