285 lines
8.1 KiB
Rust
285 lines
8.1 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 log::{debug, info};
|
|
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) => {
|
|
info!("copying dir installer file {}", path.to_string_lossy());
|
|
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<()> {
|
|
debug!("Sorting file {:?}", &file);
|
|
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(())
|
|
}
|