diff --git a/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue b/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue index 24f493ee0..dd04ae88b 100644 --- a/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue +++ b/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue @@ -107,7 +107,7 @@ defineExpose({ const install = async () => { installing.value = true - await installMod(instance.value.path, selectedVersion.value.id).catch(handleError) + await installMod(instance.value.path, selectedVersion.value.id, 'standalone').catch(handleError) installing.value = false onInstall.value(selectedVersion.value.id) incompatibleModal.value.hide() diff --git a/apps/app-frontend/src/components/ui/install_flow/ModInstallModal.vue b/apps/app-frontend/src/components/ui/install_flow/ModInstallModal.vue index 8b1edf424..243cc03e2 100644 --- a/apps/app-frontend/src/components/ui/install_flow/ModInstallModal.vue +++ b/apps/app-frontend/src/components/ui/install_flow/ModInstallModal.vue @@ -116,7 +116,7 @@ async function install(instance) { return } - await installMod(instance.path, version.id).catch(handleError) + await installMod(instance.path, version.id, 'standalone').catch(handleError) await installVersionDependencies(instance, version).catch(handleError) instance.installedMod = true @@ -188,7 +188,7 @@ const createInstance = async () => { const id = await create(name.value, gameVersion, loader, 'latest', icon.value).catch(handleError) - await installMod(id, versions.value[0].id).catch(handleError) + await installMod(id, versions.value[0].id, 'standalone').catch(handleError) await router.push(`/instance/${encodeURIComponent(id)}/`) diff --git a/apps/app-frontend/src/helpers/profile.ts b/apps/app-frontend/src/helpers/profile.ts index 7995c7004..89c43c8e4 100644 --- a/apps/app-frontend/src/helpers/profile.ts +++ b/apps/app-frontend/src/helpers/profile.ts @@ -184,8 +184,18 @@ export async function update_project(path: string, projectPath: string): Promise // Add a project to a profile from a version // Returns a path to the new project file -export async function add_project_from_version(path: string, versionId: string): Promise { - return await invoke('plugin:profile|profile_add_project_from_version', { path, versionId }) +export type DownloadReason = 'standalone' | 'dependency' | 'modpack' + +export async function add_project_from_version( + path: string, + versionId: string, + reason: DownloadReason, +): Promise { + return await invoke('plugin:profile|profile_add_project_from_version', { + path, + versionId, + reason, + }) } // Add a project to a profile from a path + project_type diff --git a/apps/app-frontend/src/pages/instance/Mods.vue b/apps/app-frontend/src/pages/instance/Mods.vue index bbf872dec..8a5a8ffe6 100644 --- a/apps/app-frontend/src/pages/instance/Mods.vue +++ b/apps/app-frontend/src/pages/instance/Mods.vue @@ -343,7 +343,7 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions } try { await remove_project(props.instance.path, mod.file_path!) - const newPath = await add_project_from_version(props.instance.path, version.id) + const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone') const profile = await get(props.instance.path).catch(handleError) if (profile) { diff --git a/apps/app-frontend/src/providers/content-install.ts b/apps/app-frontend/src/providers/content-install.ts index c57b7315d..f69ddd402 100644 --- a/apps/app-frontend/src/providers/content-install.ts +++ b/apps/app-frontend/src/providers/content-install.ts @@ -426,7 +426,7 @@ export function createContentInstall(opts: { } try { - await add_project_from_version(instance.id, version.id) + await add_project_from_version(instance.id, version.id, 'standalone') await installVersionDependencies( profile, version, @@ -484,7 +484,7 @@ export function createContentInstall(opts: { ) if (!id) return - await add_project_from_version(id, version.id) + await add_project_from_version(id, version.id, 'standalone') await opts.router.push(`/instance/${encodeURIComponent(id)}/`) const instance = await get(id) @@ -585,7 +585,7 @@ export function createContentInstall(opts: { const installedProjectIds: string[] = [project.id] addInstallingItem(instancePath, project, version) try { - await add_project_from_version(instance.path, version.id) + await add_project_from_version(instance.path, version.id, 'standalone') await installVersionDependencies( instance, version, diff --git a/apps/app-frontend/src/store/install.js b/apps/app-frontend/src/store/install.js index 3e836c436..0ed2f8a8c 100644 --- a/apps/app-frontend/src/store/install.js +++ b/apps/app-frontend/src/store/install.js @@ -177,7 +177,7 @@ export const installVersionDependencies = async (profile, version, onDepInstalli const batch = queuedInstalls.slice(i, i + batchSize) await Promise.all( batch.map(async ({ versionId }) => { - await add_project_from_version(profile.path, versionId) + await add_project_from_version(profile.path, versionId, 'dependency') }), ) } diff --git a/apps/app/src/api/profile.rs b/apps/app/src/api/profile.rs index 3d08f437f..d33d3a3f2 100644 --- a/apps/app/src/api/profile.rs +++ b/apps/app/src/api/profile.rs @@ -4,6 +4,7 @@ use path_util::SafeRelativeUtf8UnixPathBuf; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use theseus::DownloadReason; use theseus::data::{ContentItem, Dependency, LinkedModpackInfo}; use theseus::prelude::*; use theseus::profile::QuickPlayType; @@ -249,8 +250,9 @@ pub async fn profile_update_project( pub async fn profile_add_project_from_version( path: &str, version_id: &str, + reason: DownloadReason, ) -> Result { - Ok(profile::add_project_from_version(path, version_id).await?) + Ok(profile::add_project_from_version(path, version_id, reason).await?) } // Adds a project to a profile from a path diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index 53d04ad7a..d0a2e7457 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -90,6 +90,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { None, None, None, + None, Some((&loading_bar, 80.0)), &state.fetch_semaphore, &state.pool, diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index 656595fec..573aa8603 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -78,9 +78,14 @@ pub async fn import_curseforge( thumbnail_url: Some(thumbnail_url), }) = minecraft_instance.installed_modpack.clone() { - let icon_bytes = - fetch(&thumbnail_url, None, &state.fetch_semaphore, &state.pool) - .await?; + let icon_bytes = fetch( + &thumbnail_url, + None, + None, + &state.fetch_semaphore, + &state.pool, + ) + .await?; let filename = thumbnail_url.rsplit('/').next_back(); if let Some(filename) = filename { icon = Some( diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index 7e3973dfc..324b89e9f 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -4,9 +4,12 @@ use crate::data::ModLoader; use crate::event::emit::{emit_loading, init_loading}; use crate::event::{LoadingBarId, LoadingBarType}; use crate::state::{ - CacheBehaviour, CachedEntry, LinkedData, ProfileInstallStage, SideType, + CacheBehaviour, CachedEntry, LinkedData, Profile, ProfileInstallStage, + SideType, +}; +use crate::util::fetch::{ + DownloadMeta, DownloadReason, fetch, fetch_advanced, write_cached_icon, }; -use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon}; use crate::util::io; use path_util::SafeRelativeUtf8UnixPathBuf; @@ -287,12 +290,29 @@ pub async fn generate_pack_from_version_id( ) })?; + let profile = + Profile::get(&profile_path, &state.pool) + .await? + .ok_or_else(|| { + crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .as_error() + })?; + + let download_meta = DownloadMeta { + reason: DownloadReason::Modpack, + game_version: profile.game_version.clone(), + loader: profile.loader.as_str().to_string(), + }; + let file = fetch_advanced( Method::GET, &url, hash.map(|x| &**x), None, None, + Some(&download_meta), Some((&loading_bar, 70.0)), &state.fetch_semaphore, &state.pool, @@ -320,9 +340,14 @@ pub async fn generate_pack_from_version_id( emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?; let fetched = if let Some(icon_url) = project.icon_url { let state = State::get().await?; - let icon_bytes = - fetch(&icon_url, None, &state.fetch_semaphore, &state.pool) - .await?; + let icon_bytes = fetch( + &icon_url, + None, + None, + &state.fetch_semaphore, + &state.pool, + ) + .await?; let filename = icon_url.rsplit('/').next(); diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 1042cb0ad..2e4aeebb6 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -6,9 +6,12 @@ use crate::pack::install_from::{ EnvType, PackFile, PackFileHash, set_profile_information, }; use crate::state::{ - CacheBehaviour, CachedEntry, ProfileInstallStage, SideType, cache_file_hash, + CacheBehaviour, CachedEntry, Profile, ProfileInstallStage, SideType, + cache_file_hash, +}; +use crate::util::fetch::{ + DownloadMeta, DownloadReason, fetch_mirrors, sha1_async, write, }; -use crate::util::fetch::{fetch_mirrors, sha1_async, write}; use crate::util::io; use crate::{State, profile}; use async_zip::base::read::seek::ZipFileReader; @@ -207,6 +210,22 @@ pub async fn install_zipped_mrpack_files( ) .await?; + let profile = + Profile::get(&profile_path, &state.pool) + .await? + .ok_or_else(|| { + crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .as_error() + })?; + + let download_meta = DownloadMeta { + reason: DownloadReason::Modpack, + game_version: profile.game_version.clone(), + loader: profile.loader.as_str().to_string(), + }; + let num_files = pack.files.len(); loading_try_for_each_concurrent( futures::stream::iter(pack.files.into_iter()) @@ -218,6 +237,7 @@ pub async fn install_zipped_mrpack_files( None, |project| { let profile_path = profile_path.clone(); + let download_meta = download_meta.clone(); async move { //TODO: Future update: prompt user for optional files in a modpack if let Some(env) = project.env @@ -235,6 +255,7 @@ pub async fn install_zipped_mrpack_files( .map(|x| &**x) .collect::>(), project.hashes.get(&PackFileHash::Sha1).map(|x| &**x), + Some(&download_meta), &state.fetch_semaphore, &state.pool, ) diff --git a/packages/app-lib/src/api/profile/create.rs b/packages/app-lib/src/api/profile/create.rs index e017f3d93..bec35012b 100644 --- a/packages/app-lib/src/api/profile/create.rs +++ b/packages/app-lib/src/api/profile/create.rs @@ -109,6 +109,7 @@ pub async fn profile_create( let fetched = crate::util::fetch::fetch( icon, None, + None, &state.fetch_semaphore, &state.pool, ) diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs index 7a2f3c731..d7e12139a 100644 --- a/packages/app-lib/src/api/profile/mod.rs +++ b/packages/app-lib/src/api/profile/mod.rs @@ -461,6 +461,7 @@ pub async fn update_project( let mut path = Profile::add_project_version( profile_path, update_version, + fetch::DownloadReason::Standalone, &state.pool, &state.fetch_semaphore, &state.io_semaphore, @@ -501,11 +502,14 @@ pub async fn update_project( pub async fn add_project_from_version( profile_path: &str, version_id: &str, + reason: fetch::DownloadReason, ) -> crate::Result { let state = State::get().await?; + let project_path = Profile::add_project_version( profile_path, version_id, + reason, &state.pool, &state.fetch_semaphore, &state.io_semaphore, diff --git a/packages/app-lib/src/launcher/download.rs b/packages/app-lib/src/launcher/download.rs index 72fd08e4c..829ba143f 100644 --- a/packages/app-lib/src/launcher/download.rs +++ b/packages/app-lib/src/launcher/download.rs @@ -148,6 +148,7 @@ pub async fn download_client( let bytes = fetch( &client_download.url, Some(&client_download.sha1), + None, &st.fetch_semaphore, &st.pool, ) @@ -238,7 +239,7 @@ pub async fn download_assets( async { if !resource_path.exists() || force { let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &st.pool)) + .get_or_try_init(|| fetch(&url, Some(hash), None, &st.fetch_semaphore, &st.pool)) .await?; write(&resource_path, resource, &st.io_semaphore).await?; tracing::trace!("Fetched asset with hash {hash}"); @@ -252,7 +253,7 @@ pub async fn download_assets( if with_legacy && !resource_path.exists() || force { let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &st.pool)) + .get_or_try_init(|| fetch(&url, Some(hash), None, &st.fetch_semaphore, &st.pool)) .await?; write(&resource_path, resource, &st.io_semaphore).await?; tracing::trace!("Fetched legacy asset with hash {hash}"); @@ -326,6 +327,7 @@ pub async fn download_libraries( let data = fetch( &native.url, Some(&native.sha1), + None, &st.fetch_semaphore, &st.pool, ) @@ -370,6 +372,7 @@ pub async fn download_libraries( let bytes = fetch( &artifact.url, Some(&artifact.sha1), + None, &st.fetch_semaphore, &st.pool, ) @@ -406,7 +409,8 @@ pub async fn download_libraries( // failed download here is not a fatal condition. // // See DEV-479. - match fetch(&url, None, &st.fetch_semaphore, &st.pool).await + match fetch(&url, None, None, &st.fetch_semaphore, &st.pool) + .await { Ok(bytes) => { write(&path, &bytes, &st.io_semaphore).await?; @@ -465,6 +469,7 @@ pub async fn download_log_config( let bytes = fetch( &log_download.url, Some(&log_download.sha1), + None, &st.fetch_semaphore, &st.pool, ) diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index 6f62495ec..1bb282bc2 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -25,6 +25,7 @@ pub use event::{ }; pub use logger::start_logger; pub use state::State; +pub use util::fetch::DownloadReason; pub fn launcher_user_agent() -> String { const LAUNCHER_BASE_USER_AGENT: &str = diff --git a/packages/app-lib/src/state/friends.rs b/packages/app-lib/src/state/friends.rs index 41487df8c..51ad55873 100644 --- a/packages/app-lib/src/state/friends.rs +++ b/packages/app-lib/src/state/friends.rs @@ -326,6 +326,7 @@ impl FriendsSocket { None, None, None, + None, semaphore, exec, ) @@ -358,6 +359,7 @@ impl FriendsSocket { None, None, None, + None, semaphore, exec, ) diff --git a/packages/app-lib/src/state/instances/content.rs b/packages/app-lib/src/state/instances/content.rs index 1cc1a29c9..e315184b1 100644 --- a/packages/app-lib/src/state/instances/content.rs +++ b/packages/app-lib/src/state/instances/content.rs @@ -21,7 +21,9 @@ use crate::pack::install_from::{PackFileHash, PackFormat}; use crate::state::profiles::{Profile, ProfileFile, ProjectType}; use crate::state::{CacheBehaviour, CachedEntry}; -use crate::util::fetch::{FetchSemaphore, fetch_mirrors, sha1_async}; +use crate::util::fetch::{ + DownloadMeta, DownloadReason, FetchSemaphore, fetch_mirrors, sha1_async, +}; use async_zip::base::read::seek::ZipFileReader; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; @@ -319,6 +321,7 @@ pub async fn get_content_items( ); match get_modpack_identifiers( &linked_data.version_id, + profile, pool, fetch_semaphore, ) @@ -638,6 +641,7 @@ pub async fn get_linked_modpack_content( let modpack_ids = match get_modpack_identifiers( &linked_data.version_id, + profile, pool, fetch_semaphore, ) @@ -802,6 +806,7 @@ impl ModpackIdentifiers { /// Checks cache first, falls back to downloading mrpack if not cached. async fn get_modpack_identifiers( version_id: &str, + profile: &crate::state::Profile, pool: &SqlitePool, fetch_semaphore: &FetchSemaphore, ) -> crate::Result { @@ -880,9 +885,16 @@ async fn get_modpack_identifiers( )) })?; + let download_meta = DownloadMeta { + reason: DownloadReason::Modpack, + game_version: profile.game_version.clone(), + loader: profile.loader.as_str().to_string(), + }; + let mrpack_bytes = fetch_mirrors( &[&primary_file.url], primary_file.hashes.get("sha1").map(|s| s.as_str()), + Some(&download_meta), fetch_semaphore, pool, ) diff --git a/packages/app-lib/src/state/mr_auth.rs b/packages/app-lib/src/state/mr_auth.rs index 30ce2e6af..b263965eb 100644 --- a/packages/app-lib/src/state/mr_auth.rs +++ b/packages/app-lib/src/state/mr_auth.rs @@ -35,6 +35,7 @@ impl ModrinthCredentials { None, Some(("Authorization", &*creds.session)), None, + None, semaphore, exec, ) @@ -226,6 +227,7 @@ async fn fetch_info( None, Some(("Authorization", token)), None, + None, semaphore, exec, ) diff --git a/packages/app-lib/src/state/profiles.rs b/packages/app-lib/src/state/profiles.rs index e23aa4f54..1ef3d2845 100644 --- a/packages/app-lib/src/state/profiles.rs +++ b/packages/app-lib/src/state/profiles.rs @@ -1110,10 +1110,25 @@ impl Profile { pub async fn add_project_version( profile_path: &str, version_id: &str, + reason: util::fetch::DownloadReason, pool: &SqlitePool, fetch_semaphore: &FetchSemaphore, io_semaphore: &IoSemaphore, ) -> crate::Result { + let profile = + Self::get(profile_path, pool).await?.ok_or_else(|| { + crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .as_error() + })?; + + let download_meta = util::fetch::DownloadMeta { + reason, + game_version: profile.game_version.clone(), + loader: profile.loader.as_str().to_string(), + }; + let version = CachedEntry::get_version(version_id, None, pool, fetch_semaphore) .await? @@ -1139,6 +1154,7 @@ impl Profile { let bytes = util::fetch::fetch( &file.url, file.hashes.get("sha1").map(|x| &**x), + Some(&download_meta), fetch_semaphore, pool, ) diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index 627485965..d380bea94 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -9,6 +9,7 @@ use parking_lot::Mutex; use rand::Rng; use reqwest::Method; use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -17,6 +18,30 @@ use std::time::{self}; use tokio::sync::Semaphore; use tokio::{fs::File, io::AsyncWriteExt}; +pub const DOWNLOAD_META_HEADER: &str = "modrinth-download-meta"; + +#[derive(Debug, derive_more::Display, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[display(rename_all = "snake_case")] +pub enum DownloadReason { + Standalone, + Dependency, + Modpack, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DownloadMeta { + pub reason: DownloadReason, + pub game_version: String, + pub loader: String, +} + +impl DownloadMeta { + pub fn to_header_value(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } +} + #[derive(Debug)] pub struct IoSemaphore(pub Semaphore); #[derive(Debug)] @@ -156,17 +181,29 @@ const FETCH_ATTEMPTS: usize = 2; pub async fn fetch( url: &str, sha1: Option<&str>, + download_meta: Option<&DownloadMeta>, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { - fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore, exec) - .await + fetch_advanced( + Method::GET, + url, + sha1, + None, + None, + download_meta, + None, + semaphore, + exec, + ) + .await } #[tracing::instrument(skip(semaphore))] pub async fn fetch_with_client( url: &str, sha1: Option<&str>, + download_meta: Option<&DownloadMeta>, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, client: &reqwest::Client, @@ -177,6 +214,7 @@ pub async fn fetch_with_client( sha1, None, None, + download_meta, None, semaphore, exec, @@ -198,7 +236,7 @@ where T: DeserializeOwned, { let result = fetch_advanced( - method, url, sha1, json_body, None, None, semaphore, exec, + method, url, sha1, json_body, None, None, None, semaphore, exec, ) .await?; let value = serde_json::from_slice(&result)?; @@ -215,6 +253,7 @@ pub async fn fetch_advanced( sha1: Option<&str>, json_body: Option, header: Option<(&str, &str)>, + download_meta: Option<&DownloadMeta>, loading_bar: Option<(&LoadingBarId, f64)>, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, @@ -225,6 +264,7 @@ pub async fn fetch_advanced( sha1, json_body, header, + download_meta, loading_bar, semaphore, exec, @@ -242,6 +282,7 @@ pub async fn fetch_advanced_with_client( sha1: Option<&str>, json_body: Option, header: Option<(&str, &str)>, + download_meta: Option<&DownloadMeta>, loading_bar: Option<(&LoadingBarId, f64)>, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, @@ -262,12 +303,15 @@ pub async fn fetch_advanced_with_client( None }; + let download_meta_header = download_meta + .map(|m| (DOWNLOAD_META_HEADER.to_string(), m.to_header_value())); + for attempt in 1..=(FETCH_ATTEMPTS + 1) { if is_api_url && GLOBAL_FETCH_FENCE.is_blocked() { return Err(ErrorKind::ApiIsDownError.into()); } - let mut req = INSECURE_REQWEST_CLIENT.request(method.clone(), url); + let mut req = client.request(method.clone(), url); if let Some(body) = json_body.clone() { req = req.json(&body); @@ -281,6 +325,11 @@ pub async fn fetch_advanced_with_client( req = req.header("Authorization", &creds.session); } + if let Some((name, value)) = &download_meta_header { + tracing::info!("Sending download analytics: {value}"); + req = req.header(name.as_str(), value.as_str()); + } + let result = req.send().await; match result { Ok(resp) => { @@ -375,6 +424,7 @@ pub async fn fetch_advanced_with_client( pub async fn fetch_mirrors( mirrors: &[&str], sha1: Option<&str>, + download_meta: Option<&DownloadMeta>, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, ) -> crate::Result { @@ -385,9 +435,15 @@ pub async fn fetch_mirrors( } for (index, mirror) in mirrors.iter().enumerate() { - let result = - fetch_with_client(mirror, sha1, semaphore, exec, &REQWEST_CLIENT) - .await; + let result = fetch_with_client( + mirror, + sha1, + download_meta, + semaphore, + exec, + &REQWEST_CLIENT, + ) + .await; if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) { return result;