From db4d59652a6f80ad70da53370c82b1e397750776 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 4 Aug 2026 16:13:45 +0100 Subject: [PATCH] fix: better progress indicator --- .../src/components/ui/AppActionBar.vue | 2 +- packages/app-lib/src/api/pack/install_from.rs | 79 ++++++++++++++----- .../app-lib/src/api/pack/install_mrpack.rs | 76 ++++++++++++++---- packages/app-lib/src/event/emit.rs | 2 +- packages/app-lib/src/util/fetch.rs | 13 +++ 5 files changed, 135 insertions(+), 37 deletions(-) diff --git a/apps/app-frontend/src/components/ui/AppActionBar.vue b/apps/app-frontend/src/components/ui/AppActionBar.vue index 33f0a207d3..380a78a5c5 100644 --- a/apps/app-frontend/src/components/ui/AppActionBar.vue +++ b/apps/app-frontend/src/components/ui/AppActionBar.vue @@ -345,7 +345,7 @@ function buildDownloadItems(): PopupNotificationProgressItem[] { iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null, progress: getLoadingProgress(bar), waiting: !bar.total || bar.total <= 0, - progressType: 'percentage', + progressType: bar.bar_type?.type === 'pack_import' ? 'bytes' : 'percentage', progressCurrent: bar.current, progressTotal: bar.total, })), diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index a4fbf9e839..d2b7af8702 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -12,7 +12,7 @@ use crate::state::{ }; use crate::util::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 reqwest::Method; @@ -196,28 +196,59 @@ pub async fn get_instance_from_pack( }), CreatePackLocation::FromFile { path } => { let mut instance = get_local_pack_instance(&path); + let file_size = tokio::fs::metadata(&path).await?.len(); + let hashes_archive = file_size <= MAX_LOCAL_FILE_HASH_LOOKUP_SIZE; + let archive_hashing_bytes = + 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(), }, - 100.0, + inspection_total_bytes as f64, "Inspecting modpack", ) .await .ok(); - if let Some(inspection) = &inspection { - let _ = emit_loading( - inspection, - 1.0, - Some("Reading local modpack"), - ); - } + 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() - <= MAX_LOCAL_FILE_HASH_LOOKUP_SIZE - { + if let Some(inspection) = &inspection { + 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 (_, 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( &[&hash], Some(CacheBehaviour::StaleWhileRevalidateSkipOffline), @@ -239,19 +270,25 @@ pub async fn get_instance_from_pack( } else { false }; - if let Some(inspection) = &inspection { - let _ = emit_loading( - inspection, - 39.0, - Some("Inspecting modpack files"), - ); - } let external_files_in_modpack = 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?; + report_progress( + inspection_total_bytes, + 0, + "Finished inspecting modpack", + ); instance.unknown_file = !is_known_file; instance.external_files_in_modpack = external_files_in_modpack; diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 52946bd120..80d8c6f64b 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -41,6 +41,7 @@ use tokio::sync::Mutex; type ExtractProgressFn<'a> = dyn FnMut(u64) -> Pin> + Send + 'a>> + Send + 'a; +type HashProgressFn<'a> = dyn FnMut(u64) -> crate::Result<()> + Send + 'a; const MODPACK_CONTENT_DOWNLOAD_CONCURRENCY: usize = 4; #[derive(Clone)] @@ -166,13 +167,16 @@ impl MrpackZipReader { async fn hash_entry( &mut self, index: usize, + progress: Option<&mut HashProgressFn<'_>>, ) -> crate::Result<(u64, String)> { match self { 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) => { - 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( mut reader: ZipEntryReader<'_, R, WithEntry<'_>>, + mut progress: Option<&mut HashProgressFn<'_>>, ) -> crate::Result<(u64, String)> where R: futures_lite::io::AsyncBufRead + Unpin, @@ -228,6 +233,9 @@ where hasher.update(&buffer[..bytes_read]); size += bytes_read as u64; + if let Some(progress) = progress.as_mut() { + progress(bytes_read as u64)?; + } } if reader.compute_hash() != expected_crc32 { @@ -239,6 +247,7 @@ where pub(crate) async fn get_external_files_from_mrpack( file: &CreatePackFile, + mut progress: impl FnMut(u64, u64) -> crate::Result<()> + Send, ) -> crate::Result> { let mut zip_reader = MrpackZipReader::new(file).await?; let Some(manifest_idx) = @@ -271,21 +280,35 @@ pub(crate) async fn get_external_files_from_mrpack( .enumerate() .filter_map(|(index, entry)| { let path = entry.filename().as_str().ok()?; - let 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 relative_path = external_override_relative_path(path)?; let file_name = relative_path.rsplit('/').next()?.to_string(); - Some((index, file_name)) + Some((index, file_name, entry.uncompressed_size())) }) .collect::>(); - for (index, file_name) in override_entries { - let (_, hash) = zip_reader.hash_entry(index).await?; + let total_bytes = override_entries + .iter() + .map(|(_, _, size)| size) + .sum::(); + 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)); } @@ -327,6 +350,31 @@ pub(crate) async fn get_external_files_from_mrpack( Ok(external_files) } +pub(crate) async fn get_external_file_hashing_size_from_mrpack( + file: &CreatePackFile, +) -> crate::Result { + 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( mut reader: ZipEntryReader<'_, R, WithEntry<'_>>, path: &Path, @@ -527,7 +575,7 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter( .collect(); 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); } diff --git a/packages/app-lib/src/event/emit.rs b/packages/app-lib/src/event/emit.rs index 64969cd727..bc82d9fafa 100644 --- a/packages/app-lib/src/event/emit.rs +++ b/packages/app-lib/src/event/emit.rs @@ -71,7 +71,7 @@ pub async fn init_loading_unsafe( message: title.to_string(), total, current: 0.0, - last_sent: 0.0, + last_sent: -1.0, bar_type, #[cfg(feature = "cli")] cli_progress_bar: { diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index 89a440dbf9..17c0ec002e 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -954,12 +954,24 @@ pub async fn sha1_async(bytes: Bytes) -> crate::Result { pub async fn sha1_file_async( path: impl AsRef, +) -> crate::Result<(u64, String)> { + sha1_file_async_with_progress(path, |_, _| Ok(())).await +} + +pub async fn sha1_file_async_with_progress( + path: impl AsRef, + mut progress: impl FnMut(u64, u64) -> crate::Result<()>, ) -> crate::Result<(u64, String)> { let path = path.as_ref(); // Local files can be multi-gigabyte .mrpacks, so hash them without materializing bytes. let mut file = File::open(path) .await .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 size = 0; let mut buffer = vec![0; 262144]; @@ -975,6 +987,7 @@ pub async fn sha1_file_async( hasher.update(&buffer[..bytes_read]); size += bytes_read as u64; + progress(size, total)?; } Ok((size, hasher.digest().to_string()))