240 lines
6.5 KiB
Rust
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))
|
|
},
|
|
})
|
|
}
|