use futures_util::StreamExt;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use reqwest::Client;
use std::{
cmp::min,
collections::HashMap,
fs::{copy, read_dir, remove_file, rename, File},
io::Write,
};
use tokio::task::JoinSet;
use crate::{
cache::{copy_cached_version, get_cached_versions},
config::Cfg,
db::{mods_get_info, userlist_add_disabled_versions},
error::{EType, MLErr, MLE},
modrinth::Version,
List, PROGRESS_CHARS, STYLE_BAR_BYTE, STYLE_BAR_POS, STYLE_SPINNER,
};
/// # Errors
/// # Panics
pub async fn download_versions(
list: List,
config: Cfg,
versions: Vec<Version>,
progress: &MultiProgress,
progress_before: &ProgressBar,
) -> MLE<()> {
let cached = get_cached_versions(&config.cache);
let mut js = JoinSet::new();
let style_spinner = ProgressStyle::with_template(STYLE_SPINNER).unwrap();
let all = progress.insert_before(
progress_before,
ProgressBar::new(versions.len().try_into().unwrap()),
);
all.set_style(
ProgressStyle::with_template(STYLE_BAR_POS)
.unwrap()
.progress_chars(PROGRESS_CHARS),
);
all.set_message(format!("✓Downloading {}", list.id));
for ver in versions {
let p = progress.insert_before(&all, ProgressBar::new(1));
p.set_style(style_spinner.clone());
js.spawn(download_version(
config.clone(),
list.clone(),
ver,
cached.clone(),
p,
));
}
while js.join_next().await.is_some() {
all.inc(1);
}
all.finish_with_message(format!("✓Downloading {}", list.id));
Ok(())
}
/// # Errors
/// # Panics
async fn download_version(
config: Cfg,
list: List,
version: Version,
mut cached: HashMap<String, String>,
progress: ProgressBar,
) -> MLE<()> {
let project_info = mods_get_info(&config, &version.project_id)?;
let dl_path = String::from(&list.download_folder);
progress.set_message(format!("{} - {}", project_info.title, version.id));
let mut cache_msg = "";
//Check cache if already downloaded
let c = cached.remove(&version.id);
if c.is_some() {
progress.set_message(format!("Get {} from cache", version.id));
cache_msg = " (cached)";
copy_cached_version(&c.unwrap(), &dl_path);
} else {
let files = version.files;
let file = match files.clone().into_iter().find(|f| f.primary) {
Some(f) => f,
None => files[0].clone(),
};
let mut splitname: Vec<&str> = file.filename.split('.').collect();
let Ok(extension) = splitname.pop().ok_or("") else {
return Err(MLErr::new(EType::Other, "NO_FILE_EXTENSION"))
};
let filename = format!(
"{}.mr.{}.{}.{}",
splitname.join("."),
version.project_id,
version.id,
extension
);
download_file(&file.url, &list.download_folder, &filename, &progress)
.await?;
progress.set_message(format!("Copy {} to cache", version.id));
let dl_path_file = format!("{}/{}", list.download_folder, filename);
let cache_path = format!("{}/{}", &config.cache, filename);
copy(dl_path_file, cache_path)?;
}
progress.finish_with_message(format!(
"✓{} - {}{}",
project_info.title, version.id, cache_msg
));
Ok(())
}
/// # Errors
/// # Panics
async fn download_file(
url: &str,
path: &str,
name: &str,
progress: &ProgressBar,
) -> MLE<()> {
let dl_path_file = format!("{path}/{name}");
let res = Client::new().get(url).send().await?;
let size = res.content_length().expect("Couldn't get content length");
let style_bar_byte = ProgressStyle::with_template(STYLE_BAR_BYTE)
.unwrap()
.progress_chars(PROGRESS_CHARS);
progress.set_length(size);
progress.set_style(style_bar_byte);
// download chunks
let mut file = File::create(&dl_path_file)?;
let mut stream = res.bytes_stream();
let mut downloaded: u64 = 0;
while let Some(item) = stream.next().await {
// progress.inc(1);
let chunk = item?;
file.write_all(&chunk)?;
// Progress bar
let new = min(downloaded + (chunk.len() as u64), size);
downloaded = new;
progress.set_position(new);
// std::thread::sleep(std::time::Duration::from_millis(100));
}
Ok(())
}
/// # Errors
/// # Panics
pub fn disable_version(
config: &Cfg,
current_list: &List,
versionid: String,
mod_id: String,
) -> MLE<()> {
let file = get_file_path(current_list, &versionid)?;
let disabled = format!("{file}.disabled");
rename(file, disabled)?;
userlist_add_disabled_versions(config, ¤t_list.id, versionid, mod_id)?;
Ok(())
}
/// # Errors
/// # Panics
pub fn delete_version(list: &List, version: &str) -> MLE<()> {
let file = get_file_path(list, version)?;
remove_file(file)?;
Ok(())
}
/// # Errors
/// # Panics
pub fn get_file_path(list: &List, versionid: &str) -> MLE<String> {
let mut names: HashMap<String, String> = HashMap::new();
for file in read_dir(&list.download_folder)? {
let path = file?.path();
if path.is_file() {
let Ok(pathstr) = path.to_str().ok_or("") else {
return Err(MLErr::new(EType::Other, "INVALID_PATH"))
};
let namesplit: Vec<&str> = pathstr.split('.').collect();
let ver_id = namesplit[namesplit.len() - 2];
names.insert(String::from(ver_id), String::from(pathstr));
}
}
let Ok(filename) = names.get(versionid).ok_or("") else {
return Err(MLErr::new(
EType::ArgumentError,
"VERSION_NOT_FOUND_IN_FILES",
))
};
Ok(filename.to_owned())
}
/// # Errors
/// # Panics
pub fn get_downloaded_versions(list: &List) -> MLE<HashMap<String, String>> {
let mut versions: HashMap<String, String> = HashMap::new();
for file in read_dir(&list.download_folder)? {
let path = file?.path();
if path.is_file()
&& path
.extension()
.ok_or(MLErr::new(EType::IoError, "extension"))?
== "jar"
{
let pathstr = path.to_str().ok_or(MLErr::new(EType::IoError, "path_to_str"))?;
let namesplit: Vec<&str> = pathstr.split('.').collect();
versions.insert(
String::from(namesplit[namesplit.len() - 3]),
String::from(namesplit[namesplit.len() - 2]),
);
}
}
Ok(versions)
}
/// # Errors
/// # Panics
pub fn clean_list_dir(list: &List) -> MLE<()> {
let dl_path = &list.download_folder;
for entry in std::fs::read_dir(dl_path)? {
let entry = entry?;
std::fs::remove_file(entry.path())?;
}
Ok(())
}