metalos/lib/image/src/download.rs (102 lines of code) (raw):

use async_trait::async_trait; use bytes::Bytes; use futures::{Stream, StreamExt}; use reqwest::{Client, Url}; use std::io::ErrorKind; use std::pin::Pin; use thiserror::Error; use crate::{AnyImage, Result}; use btrfs::sendstream::{Sendstream, SendstreamExt, Zstd}; #[derive(Error, Debug)] pub enum Error { #[error("failure while setting up client {0}")] InitClient(reqwest::Error), #[error("format uri '{uri}' is badly formed: {error}")] InvalidUri { uri: String, error: anyhow::Error }, #[error("failure while opening http connection {0}")] Open(reqwest::Error), } #[async_trait] pub trait Downloader { type Sendstream: SendstreamExt; /// Open a [Sendstream] from the underlying image source. async fn open_sendstream(&self, image: &AnyImage) -> Result<Self::Sendstream>; } #[derive(Clone)] pub struct HttpsDownloader { client: Client, } static FORMAT_URI: &str = { #[cfg(facebook)] { "https://fbpkg.fbinfra.net/fbpkg/{package}" } #[cfg(not(facebook))] { "https://metalos/package/{package}" } }; impl HttpsDownloader { pub fn new() -> Result<Self> { // TODO: it would be nice to restrict to https only, but we use plain // http for tests, and https doesn't do much for security compared to // something like checking image signatures let client = reqwest::Client::builder() .trust_dns(true) .user_agent("metalos::image/1") .build() .map_err(Error::InitClient)?; Ok(Self { client }) } pub fn image_url(&self, img: &AnyImage) -> Result<Url> { let uri = match &img.override_uri { Some(u) => u.clone(), None => FORMAT_URI.replace("{package}", &format!("{}:{}", img.name, img.id)), }; Url::parse(&uri).map_err(|e| { Error::InvalidUri { uri, error: e.into(), } .into() }) } } impl From<HttpsDownloader> for Client { fn from(h: HttpsDownloader) -> Client { h.client } } #[async_trait] impl Downloader for HttpsDownloader { type Sendstream = Sendstream<Zstd, Pin<Box<dyn Stream<Item = std::io::Result<Bytes>> + Send>>>; async fn open_sendstream(&self, image: &AnyImage) -> Result<Self::Sendstream> { let stream = self .client .get(self.image_url(image)?) .send() .await .map_err(Error::Open)? .bytes_stream() .map(|r| r.map_err(|e| std::io::Error::new(ErrorKind::Other, e))); Ok(Sendstream::new(Box::pin(stream))) } } #[cfg(test)] mod tests { use super::*; use url::ParseError; #[test] fn image_url() -> anyhow::Result<()> { let h = HttpsDownloader::new()?; assert_eq!( format!( "{}/abc:123", FORMAT_URI.replace("{package}", "").trim_end_matches('/') ), String::from(h.image_url(&AnyImage { name: "abc".into(), id: "123".into(), kind: crate::kinds::Kind::Rootfs, override_uri: None, })?) ); Ok(()) } }