use std::path::Path;
use crate::instance::manager::InstanceManager;
use crate::instance::models::ModLoader;
use crate::net::modrinth::MrpackIndex;
use crate::tui::progress;
use super::{ImportSummary, PackFormat};
pub fn build_summary(path: &Path) -> Result<ImportSummary, String> {
let index = crate::net::modrinth::parse_mrpack(path)?;
let game_version = crate::net::modrinth::game_version_from_dependencies(&index.dependencies)
.ok_or_else(|| "Manifest missing minecraft dependency".to_string())?;
let (loader_opt, loader_version) =
crate::net::modrinth::loader_from_dependencies(&index.dependencies);
let loader = loader_opt.unwrap_or(ModLoader::Vanilla);
let override_count = count_overrides(path).unwrap_or(0);
Ok(ImportSummary {
name: index.name.clone(),
pack_version: index.version_id.clone(),
game_version,
loader,
loader_version,
mod_count: index.files.len(),
override_count,
format: PackFormat::Mrpack,
archive_path: path.to_path_buf(),
})
}
fn count_overrides(mrpack_path: &Path) -> Result<usize, String> {
let file = std::fs::File::open(mrpack_path).map_err(|e| e.to_string())?;
let archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?;
let count = archive
.file_names()
.filter(|name| {
(name.starts_with("overrides/") || name.starts_with("client-overrides/"))
&& !name.ends_with('/')
})
.count();
Ok(count)
}
pub async fn execute_import(
summary: &ImportSummary,
manager: &InstanceManager,
) -> Result<crate::instance::InstanceConfig, Box<dyn std::error::Error + Send + Sync>> {
let name = super::unique_instance_name(&summary.name, &manager.instances_dir);
progress::set_action(format!("Importing '{name}'..."));
progress::set_sub_action(format!("{} {}", summary.game_version, summary.loader));
let config = manager
.create(
&name,
&summary.game_version,
summary.loader,
summary.loader_version.as_deref(),
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;
let minecraft_dir = manager.instances_dir.join(&name).join(".minecraft");
let index = crate::net::modrinth::parse_mrpack(&summary.archive_path)
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.into() })?;
download_mod_files(&index, &minecraft_dir).await?;
extract_overrides(&summary.archive_path, &minecraft_dir)?;
progress::clear();
Ok(config)
}
async fn download_mod_files(
index: &MrpackIndex,
minecraft_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let client = crate::net::HttpClient::new();
let total = index.files.len();
let completed = Arc::new(AtomicUsize::new(0));
progress::set_action(format!("Downloading mods... 0/{total}"));
let mut tasks = tokio::task::JoinSet::new();
let max_concurrent = 10;
let mut file_iter = index.files.iter();
for _ in 0..max_concurrent {
if let Some(file) = file_iter.next() {
let client = client.clone();
let dest = minecraft_dir.join(&file.path);
let url = file.downloads.first().cloned().unwrap_or_default();
let filename = file
.path
.rsplit('/')
.next()
.unwrap_or(&file.path)
.to_string();
let completed = completed.clone();
tasks.spawn(async move {
if let Some(parent) = dest.parent() {
let _ = tokio::fs::create_dir_all(parent).await;
}
progress::set_sub_action(filename);
crate::net::download_file(&client, &url, &dest, |_, _| {}).await?;
let done = completed.fetch_add(1, Ordering::Relaxed) + 1;
progress::set_action(format!("Downloading mods... {done}/{total}"));
Ok::<(), crate::net::NetError>(())
});
}
}
for file in file_iter {
if let Some(result) = tasks.join_next().await {
result
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;
}
let client = client.clone();
let dest = minecraft_dir.join(&file.path);
let url = file.downloads.first().cloned().unwrap_or_default();
let filename = file
.path
.rsplit('/')
.next()
.unwrap_or(&file.path)
.to_string();
let completed = completed.clone();
tasks.spawn(async move {
if let Some(parent) = dest.parent() {
let _ = tokio::fs::create_dir_all(parent).await;
}
progress::set_sub_action(filename);
crate::net::download_file(&client, &url, &dest, |_, _| {}).await?;
let done = completed.fetch_add(1, Ordering::Relaxed) + 1;
progress::set_action(format!("Downloading mods... {done}/{total}"));
Ok::<(), crate::net::NetError>(())
});
}
while let Some(result) = tasks.join_next().await {
result
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;
}
Ok(())
}
fn extract_overrides(
mrpack_path: &Path,
minecraft_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::io::Read;
progress::set_action("Extracting overrides...".to_string());
progress::set_sub_action(String::new());
let file = std::fs::File::open(mrpack_path)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
let entry_name = entry.name().to_string();
let relative = entry_name
.strip_prefix("overrides/")
.or_else(|| entry_name.strip_prefix("client-overrides/"));
let Some(relative) = relative else {
continue;
};
if relative.is_empty() || entry_name.ends_with('/') {
let dir = minecraft_dir.join(relative);
std::fs::create_dir_all(dir)?;
continue;
}
let dest = minecraft_dir.join(relative);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
std::fs::write(&dest, &buf)?;
}
Ok(())
}