use crate::{ config::Config, downloader::{ Callback, CallbackStatus, DownloadError, DownloadInfo, Downloader, FileToDownload, }, forge, util::{self, CliStyle}, }; use addonscript::manifest::{ installer::Installer, link::Link, File, FileOpt, Manifest, RelationType, Repository, RepositoryType, }; use anyhow::{bail, Context}; use async_trait::async_trait; use indicatif::ProgressBar; use reqwest::Client; use std::{collections::HashMap, io::Write, path::Path, sync::Arc}; use tokio::io::AsyncReadExt; use twitch::manifest::Manifest as TwManifest; use walkdir::WalkDir; use zip::{CompressionMethod, ZipWriter}; pub async fn run((config, mut manifest): (Config, Manifest), all: bool) -> anyhow::Result<()> { util::make_tmp_dir(&config).await?; let twitch_dir = config.locations.temp_dir.join("twitch"); let mut version = manifest .versions .pop() .context("Manifest has no versions!")?; let repos = util::repo_map(manifest.repositories); let mut modloader = None; let mut cf_rels = vec![]; let mut link_rels = vec![]; for rel in version.relations { if !all && !rel.options.contains(&FileOpt::Included) { println!( "{}", format!("Skipping non-included relation {:?}", &rel).info() ); continue; } if rel.relation_type == RelationType::Modloader { if modloader.is_some() { bail!("Found multiple modloaders in manifest! Only one is supported!"); } let version = rel .versions .context("Modloader is missing `versions` field!")?; let version = forge::parse_version(&version).context("Couldn't parse forge version!")?; modloader = Some(format!("forge-{}", version)); continue; } let file = if let Some(file) = rel.file { file } else { println!( "{}", format!("Skipping relation {:?} with no files", &rel).info() ); continue; }; sort_file(file, &repos, &mut link_rels, &mut cf_rels)?; } for file in version.files { sort_file(file, &repos, &mut link_rels, &mut cf_rels)?; } let mut to_download = vec![]; for (installer, link) in link_rels { match link { Link::Http(url) => { let file_name = util::url_file_name(&url)?; let dir = if let Installer::Dir(p) = installer { p } else { bail!("Relation uses non-dir installer over http!") }; to_download.push(FileToDownload { url, target: twitch_dir.join("overrides").join(dir).join(file_name), }); }, Link::File(path) => { println!( "{}", format!("Copying local file {}", path.to_string_lossy()).info() ); match installer { Installer::Dir(dir) => { tokio::fs::copy(config.locations.src.join(path), twitch_dir.join(dir)) .await?; }, Installer::Override => { let path = config.locations.src.join(path); if !path.is_dir() { bail!("File with override installer is not directory!"); } util::copy_dir(path, twitch_dir.join("overrides")).await?; }, } }, } } struct Cb { pb: ProgressBar, } #[async_trait] impl Callback for Cb { type EndRes = Result<(), DownloadError>; type StopInfo = DownloadError; async fn on_download_complete( &mut self, res: Result, ) -> CallbackStatus { self.pb.inc(1); match res { Ok(i) => { self.pb.println(i.to_colored_text()); CallbackStatus::Continue }, Err(e) => CallbackStatus::Stop(e), } } async fn on_completed(self, stop_info: Option) -> Self::EndRes { self.pb.finish(); match stop_info { None => Ok(()), Some(e) => Err(e), } } } println!("{}", "Downloading remote files.".info()); Downloader { callback: Cb { pb: ProgressBar::new(to_download.len() as u64) .with_prefix("Downloading") .with_style(util::progress_style()), }, files: to_download, parellel_count: config.downloads.max_threads, client: Arc::new(Client::new()), } .download() .await?; println!("{}", "Creating manifest.".info()); let tw_manifest = TwManifest::create( cf_rels, &manifest .meta .contributors .into_iter() .map(|c| c.name) .collect::>(), manifest.meta.name.clone(), version.version.clone(), version.mcversion.pop().context("mcversion is empty!")?, modloader, ); let json = serde_json::to_vec(&tw_manifest).context("Failed to serialize twitch manifest")?; tokio::fs::write(twitch_dir.join("manifest.json"), json).await?; tokio::fs::create_dir_all("build").await?; println!("{}", "Zipping pack.".info()); let mut zip = ZipWriter::new( // I don't think we can make this async :( std::fs::File::create(Path::new(&format!( "build/{}-{}-twitch.zip", manifest.meta.name, version.version ))) .context("Failed to open zip file!")?, ); let options = zip::write::FileOptions::default().compression_method(CompressionMethod::Deflated); let mut buf = vec![]; for entry in WalkDir::new(&twitch_dir) { let entry = entry?; let path = entry.path(); let to = path.strip_prefix(&twitch_dir)?; if path.is_file() { zip.start_file(to.to_string_lossy(), options)?; tokio::fs::File::open(path) .await? .read_to_end(&mut buf) .await?; zip.write_all(&buf)?; buf.clear(); } else if !to.as_os_str().is_empty() { zip.add_directory(to.to_string_lossy(), options)?; } } zip.finish()?; Ok(()) } fn sort_file( file: File, repos: &HashMap, link_rels: &mut Vec<(Installer, Link)>, cf_rels: &mut Vec<(u32, u32)>, ) -> anyhow::Result<()> { match file { File::Link { installer, link, .. } => link_rels.push((installer, link)), File::Maven { installer, artifact, repository, } => { let repo = repos .get(&repository) .with_context(|| format!("File references unknown repository {}", &repository))?; match repo.repo_type { RepositoryType::Maven => { let url = util::mvn_artifact_to_url(&artifact, &repo) .context("Failed to convert maven artifact to url")?; link_rels.push((installer, Link::Http(url))); }, RepositoryType::Curseforge => { let (p_id, f_id) = util::parse_curseforge_artifact(&artifact)?; cf_rels.push(( p_id.parse() .context("Couldn't parse curseforge project ID!")?, f_id.parse().context("Couldn't parse curseforge file ID!")?, )); }, } }, } Ok(()) }