use std::{ io::{Cursor, Read}, str::FromStr, }; use tempdir::TempDir; use url::Url; use zip::ZipArchive; use super::{ contentdb::{ContentDb, ContentId}, error::{Error, Result}, minemod::{MineMod, ModId}, }; /// A source determines where a mod should be loaded from. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Source { /// Download a mod archive through HTTP. Http(Url), /// Download a mod from the Content DB, using the given user- and package name. ContentDb(ContentId), /// Search the Content DB for a given mod ID. /// /// The download may fail if there are multiple mods providing the same ID. ModId(ModId), } impl FromStr for Source { type Err = Error; fn from_str(s: &str) -> Result { if s.starts_with("http://") || s.starts_with("https://") { let url = Url::parse(s)?; return Ok(Source::Http(url)); } let groups = regex!("^([^/]+)/([^/]+)$").captures(s); if let Some(groups) = groups { return Ok(Source::ContentDb(( groups.get(1).unwrap().as_str().into(), groups.get(2).unwrap().as_str().into(), ))); } if !s.contains(' ') { return Ok(Source::ModId(s.into())); } Err(Error::InvalidSourceSpec(s.into())) } } /// A downloader is responsible for downloading mods from various sources. /// /// Note that the [`MineMod`] that the [`Downloader`] creates will not work after the downloader /// has been destroyed, as the temporary files will be lost. #[derive(Debug)] pub struct Downloader { temp_dir: TempDir, content_db: ContentDb, } impl Downloader { pub fn new() -> Result { Downloader::with_content_db(Default::default()) } pub fn with_content_db(content_db: ContentDb) -> Result { let temp_dir = TempDir::new(env!("CARGO_PKG_NAME"))?; Ok(Downloader { temp_dir, content_db, }) } pub fn download(&self, source: &Source) -> Result { match *source { Source::Http(ref url) => self.download_http(url), Source::ContentDb((ref user, ref package)) => { let url = self.content_db.download_url(user, package)?; self.download_http(&url) } Source::ModId(ref id) => { let candidates = self.content_db.resolve(id)?; if candidates.len() != 1 { return Err(Error::AmbiguousModId(id.into())); } self.download_http(&candidates[0].url) } } } /// Downloads a mod given a HTTP link. /// /// The [`Downloader`] expects to receive a zipfile containing the mod directory on this link. /// /// The mod is extracted to a temporary directory and has to be copied using /// [`MineMod::copy_to`]. pub fn download_http(&self, url: &Url) -> Result { let mut reader = ureq::request_url("GET", url).call()?.into_reader(); let mut data = Vec::new(); reader.read_to_end(&mut data)?; let data = Cursor::new(data); let mut archive = ZipArchive::new(data)?; // Here we assume that the zipfile contains only one directory, so we just take the first // name we find and extract the leading directory name. let name = archive .file_names() .next() .and_then(|name| name.split('/').next()) .ok_or(Error::EmptyArchive)? .to_string(); archive.extract(self.temp_dir.path())?; let extracted_path = self.temp_dir.path().join(name); MineMod::open(&extracted_path) } }