legacympt-rs/mpt/src/commands/downloadmods.rs
LordMZTE 814f01f389
Some checks failed
continuous-integration/drone/push Build is failing
lmpt will retry resolving artifacts
curseforge's API is so good, this endpoints only works half of the
time...
2022-03-05 16:24:39 +01:00

271 lines
7.7 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 async_trait::async_trait;
use crossterm::style::Stylize;
use futures::{stream, StreamExt};
use indicatif::ProgressBar;
use miette::{miette, IntoDiagnostic, WrapErr};
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,
) -> miette::Result<()> {
let http = Arc::new(Client::new());
let Manifest {
mut versions,
repositories,
..
} = manifest;
let version = versions
.pop()
.ok_or_else(|| miette!("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
.ok_or_else(|| miette!("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()
.ok_or_else(|| miette!("Local file has no file name!"))?,
);
tokio::fs::copy(&p, to).await.into_diagnostic()?;
println!(
"{} {}",
"Copied local file".green(),
p.to_string_lossy().cyan().bold()
);
},
Link::Http(url) => {
let file = url
.path_segments()
.ok_or_else(|| miette!("File uses base URL without path!"))?
.last()
.ok_or_else(|| miette!("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>,
) -> miette::Result<(Installer, Link)> {
Ok(match file {
File::Link {
link, installer, ..
} => (installer, link),
File::Maven {
installer,
artifact,
repository,
} => {
let repo = repos.get(&repository).ok_or_else(|| {
miette!("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 mut failed = false;
let url = loop {
let maybe_url = Url::parse(
http.get(&url)
.send()
.await
.into_diagnostic()?
.text()
.await
.into_diagnostic()?
.trim(),
);
if let (false, Err(_)) = (failed, &maybe_url) {
pb.println(
"Failed to resolve artifact, as curseforge is too incompetent to \
host a stable API. Retrying..."
.red()
.to_string(),
);
failed = true;
continue;
}
break maybe_url
.into_diagnostic()
.wrap_err("Failed to parse curseforge URL even after 2 tries lol")?;
};
pb.println(format!(
"{} {}",
"Resolved curseforge artifact with url".green(),
url.as_str().cyan().bold()
));
url
},
};
(installer, Link::Http(url))
},
})
}