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

240 lines
6.5 KiB
Rust

use crate::{
config::Config,
downloader::{
Callback,
CallbackStatus,
DownloadError,
DownloadInfo,
Downloader,
FileToDownload,
},
util::{self, mvn_artifact_to_url, progress_style, CliStyle},
};
use addonscript::manifest::{
installer::Installer,
link::Link,
File,
Manifest,
RelationType,
Repository,
RepositoryType,
};
use anyhow::Context;
use async_trait::async_trait;
use crossterm::style::Stylize;
use futures::{stream, StreamExt};
use indicatif::ProgressBar;
use percent_encoding::percent_decode;
use reqwest::Client;
use std::{
borrow::Borrow,
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use url::Url;
pub async fn run(
(config, manifest): (Config, Manifest),
target: PathBuf,
all: bool,
) -> anyhow::Result<()> {
let http = Arc::new(Client::new());
let Manifest {
mut versions,
repositories,
..
} = manifest;
let version = versions.pop().context("Manifest has no Versions!")?;
let repos = util::repo_map(repositories);
let pb = ProgressBar::new(version.relations.len() as u64)
.with_prefix("Resolving")
.with_style(progress_style());
let mut files = vec![];
for rel in version.relations {
// Only mods
if rel.relation_type != RelationType::Mod {
pb.println(
format!("Skipping non-mod relation `{}`", &rel.id)
.info()
.to_string(),
);
continue;
}
let rel_id = rel.id;
let file = rel
.file
.with_context(|| format!("Relation `{}` has no file!", rel_id))?;
if !matches!(file.installer(), Installer::Dir(d) if all || d == Path::new("mods")) {
pb.println(
format!("Skipping excluded file `{:?}`", &file)
.info()
.to_string(),
);
continue;
}
files.push(file);
}
let http_ = Arc::clone(&http);
let mut futures = stream::iter(files.into_iter())
.map(|f| process_file(f, &repos, &pb, Arc::clone(&http_)))
.buffer_unordered(config.downloads.max_threads);
let mut links = vec![];
while let Some(x) = futures.next().await {
pb.inc(1);
links.push(x?);
}
pb.finish();
let mut files = vec![];
for (installer, link) in links {
let mut to = target.clone();
if all {
let dir = if let Installer::Dir(dir) = installer {
dir
} else {
// we checked that we have a dir installer earlier
unreachable!()
};
to.push(dir);
}
match link {
Link::File(p) => {
to.push(p.file_name().context("Local file has no file name!")?);
tokio::fs::copy(&p, to).await?;
println!(
"{} {}",
"Copied local file".green(),
p.to_string_lossy().cyan().bold()
);
},
Link::Http(url) => {
let file = url
.path_segments()
.context("File uses base URL without path!")?
.last()
.context("File uses empty URL!")?;
let file = percent_decode(file.as_bytes()).decode_utf8_lossy();
to.push(Borrow::<str>::borrow(&file));
if !to.exists() {
files.push(FileToDownload { url, target: to });
}
},
}
}
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 {
Some(e) => Err(e),
None => Ok(()),
}
}
}
let pb = ProgressBar::new(files.len() as u64)
.with_prefix("Downloading")
.with_style(progress_style());
Downloader {
files,
callback: Cb { pb },
parellel_count: config.downloads.max_threads,
client: http,
}
.download()
.await?;
Ok(())
}
async fn process_file(
file: File,
repos: &HashMap<String, Repository>,
pb: &ProgressBar,
http: Arc<Client>,
) -> anyhow::Result<(Installer, Link)> {
Ok(match file {
File::Link {
link, installer, ..
} => (installer, link),
File::Maven {
installer,
artifact,
repository,
} => {
let repo = repos.get(&repository).with_context(|| {
format!("File references non-existant repository `{}`", repository)
})?;
let url = match repo.repo_type {
RepositoryType::Maven => {
let url = mvn_artifact_to_url(&artifact, &repo)?;
pb.println(format!(
"{} {}",
"Resolved maven artifact with url".green(),
url.as_str().cyan().bold()
));
url
},
RepositoryType::Curseforge => {
let (p_id, f_id) = util::parse_curseforge_artifact(&artifact)?;
let url = format!(
"https://addons-ecs.forgesvc.net/api/v2/addon/{}/file/{}/download-url",
p_id, f_id
);
let url = Url::parse(http.get(url).send().await?.text().await?.trim())
.context("failed to parse curseforge URL")?;
pb.println(format!(
"{} {}",
"Resolved curseforge artifact with url".green(),
url.as_str().cyan().bold()
));
url
},
};
(installer, Link::Http(url))
},
})
}