//! Provides utilties for downloading and parsing modpacks use daedalus::download_file; use fs_extra::dir::CopyOptions; use std::{borrow::Borrow, convert::TryFrom, env, io, path::Path}; use tokio::fs; use uuid::Uuid; use zip::ZipArchive; use self::manifest::Manifest; pub mod manifest; pub const MANIFEST_PATH: &'static str = "index.json"; pub const OVERRIDES_PATH: &'static str = "overrides/"; #[derive(thiserror::Error, Debug)] pub enum ModpackError { #[error("I/O error while reading modpack: {0}")] IOError(#[from] io::Error), #[error("I/O error while reading modpack: {0}")] FSExtraError(#[from] fs_extra::error::Error), #[error("Error extracting archive: {0}")] ZipError(#[from] zip::result::ZipError), #[error("Invalid modpack format: {0}")] FormatError(String), #[error("Invalid output directory: {0}")] InvalidDirectory(String), #[error("Error parsing manifest: {0}")] ManifestError(String), #[error("Daedalus error: {0}")] DaedalusError(#[from] daedalus::Error), #[error("Error parsing json: {0}")] JsonError(#[from] serde_json::Error), #[error("Error joining futures: {0}")] JoinError(#[from] tokio::task::JoinError), } /// Realise a modpack from a given URL pub async fn fetch_modpack( url: &str, sha1: Option<&str>, dest: &Path, side: manifest::ModpackSide, ) -> Result<(), ModpackError> { let bytes = download_file(url, sha1).await?; let mut archive = ZipArchive::new(io::Cursor::new(&bytes as &[u8]))?; realise_modpack_zip(&mut archive, dest, side).await } /// Realise a given modpack from a zip archive pub async fn realise_modpack_zip( archive: &mut ZipArchive, dest: &Path, side: manifest::ModpackSide, ) -> Result<(), ModpackError> { let tmp = env::temp_dir().join(format!("theseus-{}/", Uuid::new_v4())); archive.extract(&tmp)?; realise_modpack(&tmp, dest, side).await } /// Realise a given modpack into an instance pub async fn realise_modpack( dir: &Path, dest: &Path, side: manifest::ModpackSide, ) -> Result<(), ModpackError> { if dest.is_file() { return Err(ModpackError::InvalidDirectory(String::from( "Output is not a directory", ))); } if dest.exists() && std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) { return Err(ModpackError::InvalidDirectory(String::from( "Output directory is non-empty", ))); } if !dest.exists() { fs::create_dir_all(dest).await?; } // Copy overrides let overrides = Some(dir.join(OVERRIDES_PATH)).filter(|it| it.exists() && it.is_dir()); if let Some(overrides) = overrides { fs_extra::dir::copy(overrides, dest, &CopyOptions::new())?; } // Parse manifest // NOTE: I'm using standard files here, since Serde does not support async readers let manifest_path = Some(dir.join(MANIFEST_PATH)) .filter(|it| it.exists() && it.is_file()) .ok_or(ModpackError::ManifestError(String::from( "Manifest missing or is not a file", )))?; let manifest_file = std::fs::File::open(manifest_path)?; let manifest_json: serde_json::Value = serde_json::from_reader(io::BufReader::new(manifest_file))?; let manifest = Manifest::try_from(manifest_json)?; // Realise manifest manifest.download_files(dest, side).await?; Ok(()) }