legacympt-rs/mpt/src/commands/buildtwitch.rs

282 lines
8 KiB
Rust

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<DownloadInfo, DownloadError>,
) -> CallbackStatus<Self::StopInfo> {
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::StopInfo>) -> 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::<Vec<_>>(),
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<String, Repository>,
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(())
}