fix: show "inspecting modpack" (#6979)

* fix: show "inspecting modpack"

Shows when determining external files for modpacks which have many overrides/over gb of content files.

* fix: better progress indicator
This commit is contained in:
Calum H.
2026-08-04 15:21:31 +00:00
committed by GitHub
parent cfea3aba34
commit af7336fffb
7 changed files with 249 additions and 59 deletions
@@ -345,7 +345,7 @@ function buildDownloadItems(): PopupNotificationProgressItem[] {
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null, iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
progress: getLoadingProgress(bar), progress: getLoadingProgress(bar),
waiting: !bar.total || bar.total <= 0, waiting: !bar.total || bar.total <= 0,
progressType: 'percentage', progressType: bar.bar_type?.type === 'pack_import' ? 'bytes' : 'percentage',
progressCurrent: bar.current, progressCurrent: bar.current,
progressTotal: bar.total, progressTotal: bar.total,
})), })),
+84 -18
View File
@@ -1,5 +1,7 @@
use crate::State; use crate::State;
use crate::data::ModLoader; use crate::data::ModLoader;
use crate::event::LoadingBarType;
use crate::event::emit::{emit_loading, init_loading};
use crate::install::{ use crate::install::{
InstallErrorContext, InstallPhaseDetails, InstallPhaseId, InstallProgress, InstallErrorContext, InstallPhaseDetails, InstallPhaseId, InstallProgress,
InstallProgressReporter, InstallProgressReporter,
@@ -10,7 +12,7 @@ use crate::state::{
}; };
use crate::util::fetch::{ use crate::util::fetch::{
DownloadMeta, DownloadReason, FetchProgressFn, fetch, DownloadMeta, DownloadReason, FetchProgressFn, fetch,
fetch_advanced_with_progress, sha1_file_async, fetch_advanced_with_progress, sha1_file_async_with_progress,
}; };
use path_util::SafeRelativeUtf8UnixPathBuf; use path_util::SafeRelativeUtf8UnixPathBuf;
use reqwest::Method; use reqwest::Method;
@@ -18,7 +20,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::pin::Pin; use std::pin::Pin;
#[derive(Serialize, Deserialize, Eq, PartialEq)] #[derive(Serialize, Deserialize, Eq, PartialEq)]
@@ -153,6 +155,17 @@ pub struct CreatePack {
const MAX_LOCAL_FILE_HASH_LOOKUP_SIZE: u64 = 1024 * 1024 * 1024; const MAX_LOCAL_FILE_HASH_LOOKUP_SIZE: u64 = 1024 * 1024 * 1024;
pub(crate) fn get_local_pack_instance(path: &Path) -> CreatePackInstance {
CreatePackInstance {
name: path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
..Default::default()
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CreatePackDescription { pub struct CreatePackDescription {
pub icon: Option<PathBuf>, pub icon: Option<PathBuf>,
@@ -182,17 +195,60 @@ pub async fn get_instance_from_pack(
..Default::default() ..Default::default()
}), }),
CreatePackLocation::FromFile { path } => { CreatePackLocation::FromFile { path } => {
let file_name = path let mut instance = get_local_pack_instance(&path);
.file_stem() let file_size = tokio::fs::metadata(&path).await?.len();
.unwrap_or_default() let hashes_archive = file_size <= MAX_LOCAL_FILE_HASH_LOOKUP_SIZE;
.to_string_lossy() let archive_hashing_bytes =
.to_string(); if hashes_archive { file_size } else { 0 };
let pack_file = CreatePackFile::Path(path.clone());
let external_file_hashing_bytes =
super::install_mrpack::get_external_file_hashing_size_from_mrpack(
&pack_file,
)
.await?;
let inspection_total_bytes = archive_hashing_bytes
.saturating_add(external_file_hashing_bytes)
.max(1);
let inspection = init_loading(
LoadingBarType::PackImport {
pack_name: instance.name.clone(),
},
inspection_total_bytes as f64,
"Inspecting modpack",
)
.await
.ok();
let min_delta = (inspection_total_bytes / 200).max(256 * 1024);
let mut reported_bytes = 0_u64;
let mut report_progress =
|current: u64, offset: u64, message: &str| {
let target = offset
.saturating_add(current)
.min(inspection_total_bytes);
let increment = target.saturating_sub(reported_bytes);
if target < inspection_total_bytes && increment < min_delta
{
return;
}
let is_known_file = if tokio::fs::metadata(&path).await?.len() if let Some(inspection) = &inspection {
<= MAX_LOCAL_FILE_HASH_LOOKUP_SIZE let _ = emit_loading(
{ inspection,
increment as f64,
Some(message),
);
}
reported_bytes = target;
};
let is_known_file = if hashes_archive {
let state = State::get().await?; let state = State::get().await?;
let (_, hash) = sha1_file_async(&path).await?; let (_, hash) =
sha1_file_async_with_progress(&path, |current, _| {
report_progress(current, 0, "Hashing local modpack");
Ok(())
})
.await?;
match CachedEntry::get_file_many( match CachedEntry::get_file_many(
&[&hash], &[&hash],
Some(CacheBehaviour::StaleWhileRevalidateSkipOffline), Some(CacheBehaviour::StaleWhileRevalidateSkipOffline),
@@ -217,16 +273,26 @@ pub async fn get_instance_from_pack(
let external_files_in_modpack = let external_files_in_modpack =
super::install_mrpack::get_external_files_from_mrpack( super::install_mrpack::get_external_files_from_mrpack(
&CreatePackFile::Path(path), &pack_file,
|current, _| {
report_progress(
current,
archive_hashing_bytes,
"Inspecting modpack files",
);
Ok(())
},
) )
.await?; .await?;
report_progress(
inspection_total_bytes,
0,
"Finished inspecting modpack",
);
Ok(CreatePackInstance { instance.unknown_file = !is_known_file;
name: file_name, instance.external_files_in_modpack = external_files_in_modpack;
unknown_file: !is_known_file, Ok(instance)
external_files_in_modpack,
..Default::default()
})
} }
} }
} }
+62 -14
View File
@@ -41,6 +41,7 @@ use tokio::sync::Mutex;
type ExtractProgressFn<'a> = dyn FnMut(u64) -> Pin<Box<dyn Future<Output = crate::Result<()>> + Send + 'a>> type ExtractProgressFn<'a> = dyn FnMut(u64) -> Pin<Box<dyn Future<Output = crate::Result<()>> + Send + 'a>>
+ Send + Send
+ 'a; + 'a;
type HashProgressFn<'a> = dyn FnMut(u64) -> crate::Result<()> + Send + 'a;
const MODPACK_CONTENT_DOWNLOAD_CONCURRENCY: usize = 4; const MODPACK_CONTENT_DOWNLOAD_CONCURRENCY: usize = 4;
#[derive(Clone)] #[derive(Clone)]
@@ -166,13 +167,16 @@ impl MrpackZipReader {
async fn hash_entry( async fn hash_entry(
&mut self, &mut self,
index: usize, index: usize,
progress: Option<&mut HashProgressFn<'_>>,
) -> crate::Result<(u64, String)> { ) -> crate::Result<(u64, String)> {
match self { match self {
Self::Memory(reader) => { Self::Memory(reader) => {
hash_zip_entry(reader.reader_with_entry(index).await?).await hash_zip_entry(reader.reader_with_entry(index).await?, progress)
.await
} }
Self::File(reader) => { Self::File(reader) => {
hash_zip_entry(reader.reader_with_entry(index).await?).await hash_zip_entry(reader.reader_with_entry(index).await?, progress)
.await
} }
} }
} }
@@ -209,6 +213,7 @@ impl MrpackZipReader {
async fn hash_zip_entry<R>( async fn hash_zip_entry<R>(
mut reader: ZipEntryReader<'_, R, WithEntry<'_>>, mut reader: ZipEntryReader<'_, R, WithEntry<'_>>,
mut progress: Option<&mut HashProgressFn<'_>>,
) -> crate::Result<(u64, String)> ) -> crate::Result<(u64, String)>
where where
R: futures_lite::io::AsyncBufRead + Unpin, R: futures_lite::io::AsyncBufRead + Unpin,
@@ -228,6 +233,9 @@ where
hasher.update(&buffer[..bytes_read]); hasher.update(&buffer[..bytes_read]);
size += bytes_read as u64; size += bytes_read as u64;
if let Some(progress) = progress.as_mut() {
progress(bytes_read as u64)?;
}
} }
if reader.compute_hash() != expected_crc32 { if reader.compute_hash() != expected_crc32 {
@@ -239,6 +247,7 @@ where
pub(crate) async fn get_external_files_from_mrpack( pub(crate) async fn get_external_files_from_mrpack(
file: &CreatePackFile, file: &CreatePackFile,
mut progress: impl FnMut(u64, u64) -> crate::Result<()> + Send,
) -> crate::Result<Vec<String>> { ) -> crate::Result<Vec<String>> {
let mut zip_reader = MrpackZipReader::new(file).await?; let mut zip_reader = MrpackZipReader::new(file).await?;
let Some(manifest_idx) = let Some(manifest_idx) =
@@ -271,21 +280,35 @@ pub(crate) async fn get_external_files_from_mrpack(
.enumerate() .enumerate()
.filter_map(|(index, entry)| { .filter_map(|(index, entry)| {
let path = entry.filename().as_str().ok()?; let path = entry.filename().as_str().ok()?;
let relative_path = path let relative_path = external_override_relative_path(path)?;
.strip_prefix("overrides/")
.or_else(|| path.strip_prefix("client-overrides/"))?;
if path.ends_with('/')
|| ProjectType::get_from_parent_folder(relative_path).is_none()
{
return None;
}
let file_name = relative_path.rsplit('/').next()?.to_string(); let file_name = relative_path.rsplit('/').next()?.to_string();
Some((index, file_name)) Some((index, file_name, entry.uncompressed_size()))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (index, file_name) in override_entries { let total_bytes = override_entries
let (_, hash) = zip_reader.hash_entry(index).await?; .iter()
.map(|(_, _, size)| size)
.sum::<u64>();
let min_delta = (total_bytes / 200).max(256 * 1024);
let mut current_bytes = 0_u64;
let mut last_reported_bytes = 0_u64;
let mut report_progress = |bytes_read: u64| {
current_bytes = current_bytes.saturating_add(bytes_read);
if current_bytes >= total_bytes
|| current_bytes.saturating_sub(last_reported_bytes) < min_delta
{
return Ok(());
}
last_reported_bytes = current_bytes;
progress(current_bytes.min(total_bytes), total_bytes)
};
for (index, file_name, _) in override_entries {
let (_, hash) = zip_reader
.hash_entry(index, Some(&mut report_progress))
.await?;
candidates.push((file_name, hash)); candidates.push((file_name, hash));
} }
@@ -327,6 +350,31 @@ pub(crate) async fn get_external_files_from_mrpack(
Ok(external_files) Ok(external_files)
} }
pub(crate) async fn get_external_file_hashing_size_from_mrpack(
file: &CreatePackFile,
) -> crate::Result<u64> {
let zip_reader = MrpackZipReader::new(file).await?;
Ok(zip_reader
.file()
.entries()
.iter()
.filter_map(|entry| {
let path = entry.filename().as_str().ok()?;
external_override_relative_path(path)
.map(|_| entry.uncompressed_size())
})
.sum())
}
fn external_override_relative_path(path: &str) -> Option<&str> {
let relative_path = path
.strip_prefix("overrides/")
.or_else(|| path.strip_prefix("client-overrides/"))?;
(!path.ends_with('/')
&& ProjectType::get_from_parent_folder(relative_path).is_some())
.then_some(relative_path)
}
async fn extract_zip_entry<R>( async fn extract_zip_entry<R>(
mut reader: ZipEntryReader<'_, R, WithEntry<'_>>, mut reader: ZipEntryReader<'_, R, WithEntry<'_>>,
path: &Path, path: &Path,
@@ -527,7 +575,7 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
.collect(); .collect();
for index in override_entries { for index in override_entries {
let (_, hash) = zip_reader.hash_entry(index).await?; let (_, hash) = zip_reader.hash_entry(index, None).await?;
file_hashes.push(hash); file_hashes.push(hash);
} }
+1 -1
View File
@@ -71,7 +71,7 @@ pub async fn init_loading_unsafe(
message: title.to_string(), message: title.to_string(),
total, total,
current: 0.0, current: 0.0,
last_sent: 0.0, last_sent: -1.0,
bar_type, bar_type,
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
cli_progress_bar: { cli_progress_bar: {
+3
View File
@@ -157,6 +157,9 @@ pub enum LoadingBarType {
pack_id: Option<String>, pack_id: Option<String>,
pack_version: Option<String>, pack_version: Option<String>,
}, },
PackImport {
pack_name: String,
},
MinecraftDownload { MinecraftDownload {
instance_id: String, instance_id: String,
instance_name: String, instance_name: String,
+85 -25
View File
@@ -16,6 +16,7 @@ use crate::ErrorKind;
use crate::api::pack::install_from::{ use crate::api::pack::install_from::{
CreatePackLocation, generate_pack_from_file, CreatePackLocation, generate_pack_from_file,
generate_pack_from_version_id_with_reporter, get_instance_from_pack, generate_pack_from_version_id_with_reporter, get_instance_from_pack,
get_local_pack_instance,
}; };
use crate::api::pack::install_mrpack::install_zipped_mrpack_files_with_reporter; use crate::api::pack::install_mrpack::install_zipped_mrpack_files_with_reporter;
use crate::event::InstancePayloadType; use crate::event::InstancePayloadType;
@@ -163,16 +164,6 @@ pub async fn retry_job(job_id: Uuid) -> crate::Result<InstallJobSnapshot> {
job.state.progress.phase = InstallPhaseId::PreparingInstance; job.state.progress.phase = InstallPhaseId::PreparingInstance;
job.state.progress.progress = None; job.state.progress.progress = None;
job.state.progress.details = InstallPhaseDetails::Empty; job.state.progress.details = InstallPhaseDetails::Empty;
if let Err(error) = prepare_initial_instance(&mut job.state, &state).await {
if let Err(cleanup_error) =
recovery::apply_cleanup(&job.state, &state).await
{
tracing::error!(
"Error cleaning up install job {job_id} retry preparation: {cleanup_error}"
);
}
return Err(error);
}
job.state.record_event(InstallJobEventKind::JobQueued { job.state.record_event(InstallJobEventKind::JobQueued {
kind: job.state.request.kind(), kind: job.state.request.kind(),
}); });
@@ -197,6 +188,42 @@ pub async fn retry_job(job_id: Uuid) -> crate::Result<InstallJobSnapshot> {
return Err(error); return Err(error);
} }
}; };
emit_install_job(&record.snapshot()).await?;
if let Err(error) = prepare_initial_instance(&mut job.state, &state).await {
let error_view = install_error_view(
job.state.progress.phase,
&error,
job.state.context.clone(),
);
if let Err(terminal_error) =
terminalize_failed_job(job_id, job.state, error_view, &state).await
{
tracing::error!(
"Failed to terminalize retried install job {job_id}: {terminal_error}"
);
}
return Err(error);
}
let record = match store::update_state(job_id, &job.state, &state).await {
Ok(record) => record,
Err(error) => {
let error_view = install_error_view(
job.state.progress.phase,
&error,
job.state.context.clone(),
);
if let Err(terminal_error) =
terminalize_failed_job(job_id, job.state, error_view, &state)
.await
{
tracing::error!(
"Failed to terminalize retried install job {job_id}: {terminal_error}"
);
}
return Err(error);
}
};
if let Err(error) = if let Err(error) =
lock_existing_instance_if_needed(&job.state, &state).await lock_existing_instance_if_needed(&job.state, &state).await
{ {
@@ -337,31 +364,39 @@ async fn start(request: InstallRequest) -> crate::Result<InstallJobSnapshot> {
let state = State::get().await?; let state = State::get().await?;
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let mut job_state = InstallJobState::new(request); let mut job_state = InstallJobState::new(request);
set_initial_display(&mut job_state);
let record =
store::insert(id, &job_state, InstallJobStatus::Queued, &state).await?;
emit_install_job(&record.snapshot()).await?;
if let Err(error) = prepare_initial_instance(&mut job_state, &state).await { if let Err(error) = prepare_initial_instance(&mut job_state, &state).await {
if let Err(cleanup_error) = let error_view = install_error_view(
recovery::apply_cleanup(&job_state, &state).await job_state.progress.phase,
&error,
job_state.context.clone(),
);
if let Err(terminal_error) =
terminalize_failed_job(id, job_state, error_view, &state).await
{ {
tracing::error!( tracing::error!(
"Error cleaning up install job preparation: {cleanup_error}" "Failed to terminalize install job {id} after setup error: {terminal_error}"
); );
} }
return Err(error); return Err(error);
} }
let record = match store::insert( let record = match store::update_state(id, &job_state, &state).await {
id,
&job_state,
InstallJobStatus::Queued,
&state,
)
.await
{
Ok(record) => record, Ok(record) => record,
Err(error) => { Err(error) => {
if let Err(cleanup_error) = let error_view = install_error_view(
recovery::apply_cleanup(&job_state, &state).await job_state.progress.phase,
&error,
job_state.context.clone(),
);
if let Err(terminal_error) =
terminalize_failed_job(id, job_state, error_view, &state).await
{ {
tracing::error!( tracing::error!(
"Error cleaning up untracked install job {id}: {cleanup_error}" "Failed to terminalize install job {id} after setup error: {terminal_error}"
); );
} }
return Err(error); return Err(error);
@@ -422,7 +457,12 @@ async fn prepare_initial_instance(
location, location,
post_install_edit, post_install_edit,
} => { } => {
let preview = get_instance_from_pack(location).await?; let preview = match location {
CreatePackLocation::FromFile { path } => {
get_local_pack_instance(&path)
}
location => get_instance_from_pack(location).await?,
};
let name = post_install_edit let name = post_install_edit
.as_ref() .as_ref()
.and_then(|edit| edit.name.clone()) .and_then(|edit| edit.name.clone())
@@ -1443,6 +1483,26 @@ fn set_display(
job_state.display = Some(InstallJobDisplay { title, icon }); job_state.display = Some(InstallJobDisplay { title, icon });
} }
fn set_initial_display(job_state: &mut InstallJobState) {
let display = match &job_state.request {
InstallRequest::CreateModpackInstance { location, .. } => {
match location {
CreatePackLocation::FromVersionId {
title, icon_url, ..
} => Some((title.clone(), icon_url.clone())),
CreatePackLocation::FromFile { path } => {
Some((get_local_pack_instance(path).name, None))
}
}
}
_ => None,
};
if let Some((title, icon)) = display {
set_display(job_state, title, icon);
}
}
fn install_error_view( fn install_error_view(
phase: InstallPhaseId, phase: InstallPhaseId,
error: &crate::Error, error: &crate::Error,
+13
View File
@@ -954,12 +954,24 @@ pub async fn sha1_async(bytes: Bytes) -> crate::Result<String> {
pub async fn sha1_file_async( pub async fn sha1_file_async(
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> crate::Result<(u64, String)> {
sha1_file_async_with_progress(path, |_, _| Ok(())).await
}
pub async fn sha1_file_async_with_progress(
path: impl AsRef<Path>,
mut progress: impl FnMut(u64, u64) -> crate::Result<()>,
) -> crate::Result<(u64, String)> { ) -> crate::Result<(u64, String)> {
let path = path.as_ref(); let path = path.as_ref();
// Local files can be multi-gigabyte .mrpacks, so hash them without materializing bytes. // Local files can be multi-gigabyte .mrpacks, so hash them without materializing bytes.
let mut file = File::open(path) let mut file = File::open(path)
.await .await
.map_err(|e| IOError::with_path(e, path))?; .map_err(|e| IOError::with_path(e, path))?;
let total = file
.metadata()
.await
.map_err(|e| IOError::with_path(e, path))?
.len();
let mut hasher = sha1_smol::Sha1::new(); let mut hasher = sha1_smol::Sha1::new();
let mut size = 0; let mut size = 0;
let mut buffer = vec![0; 262144]; let mut buffer = vec![0; 262144];
@@ -975,6 +987,7 @@ pub async fn sha1_file_async(
hasher.update(&buffer[..bytes_read]); hasher.update(&buffer[..bytes_read]);
size += bytes_read as u64; size += bytes_read as u64;
progress(size, total)?;
} }
Ok((size, hasher.digest().to_string())) Ok((size, hasher.digest().to_string()))