#![warn(
// Harden built-in lints
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
unreachable_pub,
// Harden clippy lints
clippy::clone_on_ref_ptr,
clippy::dbg_macro,
clippy::decimal_literal_representation,
clippy::float_cmp_const,
clippy::get_unwrap,
clippy::integer_arithmetic,
clippy::integer_division,
clippy::pedantic,
clippy::print_stdout,
)]
use std::{
ffi::OsStr,
fmt,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Duration,
};
use anyhow::{anyhow, Error};
use flate2::read::GzDecoder;
use ureq::{Agent, Response};
use url::Url;
#[derive(Debug)]
pub struct Fetcher {
pub client: Agent,
}
impl Default for Fetcher {
fn default() -> Self {
Self {
client: ureq::agent().build(),
}
}
}
impl Fetcher {
pub fn new() -> Self {
Self::default()
}
pub fn open<F: Fetchable>(&mut self, resource: F) -> Result<F::Reader, F::Error> {
resource.reader_for(self)
}
}
pub trait Fetchable {
type Reader: Read;
type Error;
fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error>;
}
impl Fetchable for &str {
type Reader = Box<dyn Read>;
type Error = Error;
fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
match Url::parse(self) {
Ok(path) => path.reader_for(f),
Err(_) => Path::new(self).reader_for(f),
}
}
}
impl Fetchable for Url {
type Reader = Box<dyn Read>;
type Error = Error;
fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
match self.to_file_path() {
Ok(path) => path.reader_for(f),
Err(()) => f
.client
.get(self.as_str())
.timeout(Duration::from_secs(3))
.call()
.reader_for(f),
}
}
}
impl Fetchable for Response {
type Reader = Box<dyn Read>;
type Error = Error;
fn reader_for(self, _f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
if self.header("Content-Type").map_or(false, |v| v == "application/x-gzip")
|| self
.header("Content-Disposition")
.map_or(false, |v| v.contains(".gz"))
{
Ok(Box::new(GzDecoder::new(self.into_reader())))
} else {
Ok(Box::new(self.into_reader()))
}
}
}
impl Fetchable for &Path {
type Reader = Box<dyn Read>;
type Error = Error;
fn reader_for(self, _f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
let file = File::open(self)?;
if self.extension().map_or(false, |ext| ext == "gz") {
Ok(Box::new(GzDecoder::new(file)))
} else {
Ok(Box::new(file))
}
}
}
impl Fetchable for PathBuf {
type Reader = Box<dyn Read>;
type Error = Error;
fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
(&*self).reader_for(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Resource {
Url(Url),
PathBuf(PathBuf),
}
impl Fetchable for Resource {
type Reader = Box<dyn Read>;
type Error = Error;
fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
match self {
Self::Url(url) => url.reader_for(f),
Self::PathBuf(path) => path.reader_for(f),
}
}
}
impl Resource {
pub fn is_absolute(&self) -> bool {
match self {
Self::Url(_) => true,
Self::PathBuf(path) => path.is_absolute(),
}
}
pub fn join(mut self, other: Self) -> Result<Self, Error> {
if other.is_absolute() {
return Ok(other);
}
let other_str = other.as_ref().to_str().ok_or(anyhow!("UTF-8 error"))?;
match self {
Self::Url(parent) => parent.join(other_str).map(Self::Url).map_err(Error::from),
Self::PathBuf(ref mut parent) => {
parent.set_file_name(other_str);
Ok(self)
}
}
}
}
impl From<PathBuf> for Resource {
fn from(path: PathBuf) -> Self {
Self::PathBuf(path)
}
}
impl From<Url> for Resource {
fn from(url: Url) -> Self {
Self::Url(url)
}
}
impl From<String> for Resource {
fn from(s: String) -> Self {
match Url::parse(&s) {
Ok(path) => Self::Url(path),
Err(_) => Self::PathBuf(PathBuf::from(s)),
}
}
}
impl From<&str> for Resource {
fn from(s: &str) -> Self {
match Url::parse(s) {
Ok(path) => Self::Url(path),
Err(_) => Self::PathBuf(PathBuf::from(s)),
}
}
}
impl AsRef<OsStr> for Resource {
fn as_ref(&self) -> &OsStr {
match self {
Self::Url(url) => OsStr::new(url.as_str()),
Self::PathBuf(path) => OsStr::new(path),
}
}
}
impl fmt::Display for Resource {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Url(url) => url.fmt(f),
Self::PathBuf(path) => path.display().fmt(f),
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Resource {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Url(url) => serializer.serialize_str(url.as_str()),
Self::PathBuf(path) => path.serialize(serializer),
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Resource {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
<&str>::deserialize(deserializer).map(Self::from)
}
}