From 2db71ce4270eafcc7292bf49903b3296de1aeee2 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Sun, 1 Nov 2020 14:24:34 -0800 Subject: [PATCH] fix catalog install flow - Do not insert the catalog addon into our main addons vec until it has been successfully unpacked. - Make sure any existing addon that has the same addon folders as our newly installed addon is removed prior to inserting the new addon to prevent dupliates. - Make sure addon cache entries are removed when the addon is deleted or when the same addon is installed via catalog from the Curse source --- crates/core/src/addon.rs | 33 +++ crates/core/src/cache.rs | 39 ++- crates/core/src/fs/addon.rs | 3 +- src/gui/element.rs | 15 +- src/gui/mod.rs | 27 +- src/gui/update.rs | 473 +++++++++++++++++------------------- 6 files changed, 311 insertions(+), 279 deletions(-) diff --git a/crates/core/src/addon.rs b/crates/core/src/addon.rs index 9832a379..dca12226 100644 --- a/crates/core/src/addon.rs +++ b/crates/core/src/addon.rs @@ -870,6 +870,39 @@ impl Addon { } } } + + pub fn update_addon_folders(&mut self, folders: &[AddonFolder]) { + let mut folders = folders.to_vec(); + + if !folders.is_empty() { + folders.sort_by(|a, b| a.id.cmp(&b.id)); + + // Assign the primary folder id based on the first folder alphabetically with + // a matching repository identifier otherwise just the first + // folder alphabetically + let primary_folder_id = if let Some(folder) = folders.iter().find(|f| { + if let Some(repo) = self.active_repository { + match repo { + Repository::Curse => { + self.repository_id() + == f.repository_identifiers.curse.as_ref().map(u32::to_string) + } + Repository::Tukui => self.repository_id() == f.repository_identifiers.tukui, + Repository::WowI => self.repository_id() == f.repository_identifiers.wowi, + } + } else { + false + } + }) { + folder.id.clone() + } else { + // Wont fail since we already checked if vec is empty + folders.get(0).map(|f| f.id.clone()).unwrap() + }; + self.primary_folder_id = primary_folder_id; + self.folders = folders; + } + } } impl PartialEq for Addon { diff --git a/crates/core/src/cache.rs b/crates/core/src/cache.rs index 674ab12f..f3369200 100644 --- a/crates/core/src/cache.rs +++ b/crates/core/src/cache.rs @@ -73,6 +73,9 @@ pub async fn load_addon_cache() -> Result { AddonCache::load_or_default() } +/// Update the cache with input entry. If an entry already exists in the cache, +/// with the same folder names as the input entry, that entry will be deleted +/// before inserting the input entry. pub async fn update_addon_cache( addon_cache: Arc>, entry: AddonCacheEntry, @@ -85,7 +88,7 @@ pub async fn update_addon_cache( let entries = addon_cache.get_mut_for_flavor(flavor); // Remove old entry, if it exists - entries.retain(|e| e.title != entry.title); + entries.retain(|e| e.folder_names != entry.folder_names); // Add new entry entries.push(entry.clone()); @@ -96,6 +99,35 @@ pub async fn update_addon_cache( Ok(entry) } +/// Remove the cache entry that has the same folder names +/// as the input entry. Will return the removed entry, if applicable. +pub async fn remove_addon_cache_entry( + addon_cache: Arc>, + entry: AddonCacheEntry, + flavor: Flavor, +) -> Option { + // Lock mutex to get mutable access and block other tasks from trying to update + let mut addon_cache = addon_cache.lock().await; + + // Get entries for flavor + let entries = addon_cache.get_mut_for_flavor(flavor); + + // Remove old entry, if it exists + if let Some(idx) = entries + .iter() + .position(|e| e.folder_names == entry.folder_names) + { + let entry = entries.remove(idx); + + // Persist changes to filesystem + let _ = addon_cache.save(); + + Some(entry) + } else { + None + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct AddonCacheEntry { pub title: String, @@ -113,12 +145,15 @@ impl TryFrom<&Addon> for AddonCacheEntry { if let (Some(repository), Some(repository_id)) = (addon.active_repository, addon.repository_id()) { + let mut folder_names: Vec<_> = addon.folders.iter().map(|a| a.id.clone()).collect(); + folder_names.sort(); + Ok(AddonCacheEntry { title: addon.title().to_owned(), repository, repository_id, primary_folder_id: addon.primary_folder_id.clone(), - folder_names: addon.folders.iter().map(|a| a.id.clone()).collect(), + folder_names, modified: Utc::now(), }) } else { diff --git a/crates/core/src/fs/addon.rs b/crates/core/src/fs/addon.rs index 2ebca0ee..000b1fba 100644 --- a/crates/core/src/fs/addon.rs +++ b/crates/core/src/fs/addon.rs @@ -77,7 +77,8 @@ pub async fn install_addon( // Cleanup std::fs::remove_file(&zip_path)?; - let addon_folders = toc_files.iter().filter_map(parse_toc_path).collect(); + let mut addon_folders: Vec<_> = toc_files.iter().filter_map(parse_toc_path).collect(); + addon_folders.sort(); Ok(addon_folders) } diff --git a/src/gui/element.rs b/src/gui/element.rs index af6ac302..b49b8d2c 100644 --- a/src/gui/element.rs +++ b/src/gui/element.rs @@ -3,9 +3,9 @@ use { super::{ style, AddonVersionKey, BackupState, CatalogColumnKey, CatalogColumnSettings, - CatalogColumnState, CatalogInstallStatus, CatalogRow, Changelog, ColumnKey, ColumnSettings, - ColumnState, DirectoryType, ExpandType, Interaction, Message, Mode, ReleaseChannel, - ScaleState, SelfUpdateState, SortDirection, State, ThemeState, + CatalogColumnState, CatalogInstallAddon, CatalogInstallStatus, CatalogRow, Changelog, + ColumnKey, ColumnSettings, ColumnState, DirectoryType, ExpandType, Interaction, Message, + Mode, ReleaseChannel, ScaleState, SelfUpdateState, SortDirection, State, ThemeState, }, crate::VERSION, ajour_core::{ @@ -1816,7 +1816,7 @@ pub fn catalog_data_cell<'a, 'b>( addon: &'a mut CatalogRow, column_config: &'b [(CatalogColumnKey, Length, bool)], installed_for_flavor: bool, - statuses: Vec<(Flavor, CatalogInstallStatus)>, + install_addon: Option<&CatalogInstallAddon>, ) -> Container<'a, Message> { let default_height = Length::Units(26); @@ -1843,10 +1843,7 @@ pub fn catalog_data_cell<'a, 'b>( }) .next() { - let status = statuses - .iter() - .find(|(f, _)| *f == config.wow.flavor) - .map(|(_, status)| *status); + let status = install_addon.map(|a| a.status); let install_text = Text::new(if !flavor_exists_for_addon { "N/A" @@ -1854,8 +1851,6 @@ pub fn catalog_data_cell<'a, 'b>( match status { Some(CatalogInstallStatus::Downloading) => "Downloading", Some(CatalogInstallStatus::Unpacking) => "Unpacking", - Some(CatalogInstallStatus::Fingerprint) => "Hashing", - Some(CatalogInstallStatus::Completed) => "Installed", Some(CatalogInstallStatus::Retry) => "Retry", Some(CatalogInstallStatus::Unavilable) => "Unavailable", None => { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index e8eae1f5..5b7f3889 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -113,7 +113,7 @@ pub enum Message { None(()), Parse(()), ParsedAddons((Flavor, Result>)), - UpdateFingerprint((DownloadReason, Flavor, String, Result<()>)), + UpdateFingerprint((Flavor, String, Result<()>)), ThemeSelected(String), ReleaseChannelSelected(ReleaseChannel), ThemesLoaded(Vec), @@ -128,6 +128,7 @@ pub enum Message { FetchedChangelog((Addon, AddonVersionKey, Result<(String, String)>)), AjourUpdateDownloaded(Result<(String, PathBuf)>), AddonCacheUpdated(Result), + AddonCacheEntryRemoved(Option), } pub struct Ajour { @@ -163,7 +164,7 @@ pub struct Ajour { catalog_column_settings: CatalogColumnSettings, onboarding_directory_btn_state: button::State, catalog: Option, - catalog_install_statuses: Vec<(Flavor, u32, CatalogInstallStatus)>, + catalog_install_addons: HashMap>, catalog_search_state: CatalogSearchState, catalog_header_state: CatalogHeaderState, website_btn_state: button::State, @@ -210,7 +211,7 @@ impl Default for Ajour { catalog_column_settings: Default::default(), onboarding_directory_btn_state: Default::default(), catalog: None, - catalog_install_statuses: vec![], + catalog_install_addons: Default::default(), catalog_search_state: Default::default(), catalog_header_state: Default::default(), website_btn_state: Default::default(), @@ -523,6 +524,8 @@ impl Application for Ajour { &mut self.catalog_search_state.scrollable_state, ); + let install_addons = self.catalog_install_addons.entry(flavor).or_default(); + for addon in self.catalog_search_state.catalog_rows.iter_mut() { // TODO: We should make this prettier with new sources coming in. let installed_for_flavor = addons.iter().any(|a| { @@ -530,12 +533,7 @@ impl Application for Ajour { || a.tukui_id() == Some(&addon.addon.id.to_string()) }); - let statuses = self - .catalog_install_statuses - .iter() - .filter(|(_, i, _)| addon.addon.id == *i) - .map(|(flavor, _, status)| (*flavor, *status)) - .collect(); + let install_addon = install_addons.iter().find(|a| addon.addon.id == a.id); let catalog_data_cell = element::catalog_data_cell( color_palette, @@ -543,7 +541,7 @@ impl Application for Ajour { addon, &catalog_column_config, installed_for_flavor, - statuses, + install_addon, ); catalog_scrollable = catalog_scrollable.push(catalog_data_cell); @@ -1241,12 +1239,17 @@ impl From for CatalogRow { } } +#[derive(Debug, Clone, PartialEq)] +pub struct CatalogInstallAddon { + id: u32, + status: CatalogInstallStatus, + addon: Option, +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum CatalogInstallStatus { Downloading, Unpacking, - Fingerprint, - Completed, Retry, Unavilable, } diff --git a/src/gui/update.rs b/src/gui/update.rs index 0f3acb37..8e86ed5c 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -1,14 +1,17 @@ use { super::{ - AddonVersionKey, Ajour, CatalogCategory, CatalogColumnKey, CatalogInstallStatus, - CatalogRow, CatalogSource, Changelog, ChangelogPayload, ColumnKey, DirectoryType, - DownloadReason, ExpandType, Interaction, Message, Mode, SelfUpdateStatus, SortDirection, - State, + AddonVersionKey, Ajour, CatalogCategory, CatalogColumnKey, CatalogInstallAddon, + CatalogInstallStatus, CatalogRow, CatalogSource, Changelog, ChangelogPayload, ColumnKey, + DirectoryType, DownloadReason, ExpandType, Interaction, Message, Mode, SelfUpdateStatus, + SortDirection, State, }, ajour_core::{ addon::{Addon, AddonFolder, AddonState, Repository}, backup::{backup_folders, latest_backup, BackupFolder}, - cache::{update_addon_cache, AddonCache, AddonCacheEntry, FingerprintCache}, + cache::{ + remove_addon_cache_entry, update_addon_cache, AddonCache, AddonCacheEntry, + FingerprintCache, + }, catalog, config::{ColumnConfig, ColumnConfigV2, Flavor}, curse_api, @@ -384,6 +387,22 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { + return Ok(Command::perform( + remove_addon_cache_entry(addon_cache.clone(), entry, flavor), + Message::AddonCacheEntryRemoved, + )); + } + _ => {} + } + } + } } } Message::Interaction(Interaction::Update(id)) => { @@ -532,70 +551,70 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { - // Update catalog status for addon - if reason == DownloadReason::Install { - update_catalog_install_status( - &mut ajour.catalog_install_statuses, - CatalogInstallStatus::Unpacking, - flavor, - addon.repository_id(), - ); + match result { + Ok(_) => match reason { + DownloadReason::Update => { + if let Some(_addon) = addons.iter_mut().find(|a| a.primary_folder_id == id) + { + addon = Some(_addon); } + } + DownloadReason::Install => { + if let Some(install_addon) = install_addons + .iter_mut() + .find(|a| a.addon.as_ref().map(|a| &a.primary_folder_id) == Some(&id)) + { + install_addon.status = CatalogInstallStatus::Unpacking; - if addon.state == AddonState::Downloading { - addon.state = AddonState::Unpacking; - let addon = addon.clone(); - return Ok(Command::perform( - perform_unpack_addon( - reason, - flavor, - addon, - from_directory, - to_directory, - ), - Message::UnpackedAddon, - )); + if let Some(_addon) = install_addon.addon.as_mut() { + addon = Some(_addon); + } } } - Err(error) => { - log::error!("{}", error); - ajour.error = Some(error.to_string()); - - // Update catalog status for addon - if reason == DownloadReason::Install { - update_catalog_install_status( - &mut ajour.catalog_install_statuses, - CatalogInstallStatus::Retry, - flavor, - addon.repository_id(), - ); + }, + Err(error) => { + log::error!("{}", error); + ajour.error = Some(error.to_string()); - remove_catalog_addon = Some(addon.primary_folder_id.clone()); + if reason == DownloadReason::Install { + if let Some(install_addon) = + install_addons.iter_mut().find(|a| a.id.to_string() == id) + { + install_addon.status = CatalogInstallStatus::Retry; } } } } - // Remove catalog installed addon from addons since it failed - if let Some(id) = remove_catalog_addon { - addons.retain(|a| a.primary_folder_id != id) + if let Some(addon) = addon { + let from_directory = ajour + .config + .get_download_directory_for_flavor(flavor) + .expect("Expected a valid path"); + let to_directory = ajour + .config + .get_addon_directory_for_flavor(&flavor) + .expect("Expected a valid path"); + + if addon.state == AddonState::Downloading { + addon.state = AddonState::Unpacking; + + return Ok(Command::perform( + perform_unpack_addon( + reason, + flavor, + addon.clone(), + from_directory, + to_directory, + ), + Message::UnpackedAddon, + )); + } } } Message::UnpackedAddon((reason, flavor, id, result)) => { @@ -605,116 +624,122 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { - // Update the folders of the addon since they could have changed from the update, - // or if its an addon installed through the catalog, we haven't assigned it folders yet - if !folders.is_empty() { - folders.sort_by(|a, b| a.id.cmp(&b.id)); - - // Assign the primary folder id based on the first folder alphabetically with - // a matching repository identifier otherwise just the first - // folder alphabetically - let primary_folder_id = if let Some(folder) = folders.iter().find(|f| { - if let Some(repo) = addon.active_repository { - match repo { - Repository::Curse => { - addon.repository_id() - == f.repository_identifiers - .curse - .as_ref() - .map(u32::to_string) - } - Repository::Tukui => { - addon.repository_id() == f.repository_identifiers.tukui - } - Repository::WowI => { - addon.repository_id() == f.repository_identifiers.wowi - } - } - } else { - false - } - }) { - folder.id.clone() - } else { - // Wont fail since we already checked if vec is empty - folders.get(0).map(|f| f.id.clone()).unwrap() - }; - addon.primary_folder_id = primary_folder_id; - addon.folders = folders; - } + let install_addons = ajour.catalog_install_addons.entry(flavor).or_default(); - // Update catalog status for addon - if reason == DownloadReason::Install { - update_catalog_install_status( - &mut ajour.catalog_install_statuses, - CatalogInstallStatus::Fingerprint, - flavor, - addon.repository_id(), - ); + let mut addon = None; + let mut folders = None; + + match result { + Ok(_folders) => match reason { + DownloadReason::Update => { + if let Some(_addon) = addons.iter_mut().find(|a| a.primary_folder_id == id) + { + addon = Some(_addon); + folders = Some(_folders); + } + } + DownloadReason::Install => { + if let Some(install_addon) = install_addons + .iter_mut() + .find(|a| a.addon.as_ref().map(|a| &a.primary_folder_id) == Some(&id)) + { + if let Some(_addon) = install_addon.addon.as_mut() { + // If we are installing from the catalog, remove any existing addon + // that has the same folders and insert this new one + addons.retain(|a| a.folders != _folders); + addons.push(_addon.clone()); + + addon = addons.iter_mut().find(|a| a.primary_folder_id == id); + folders = Some(_folders); + } } - addon.state = AddonState::Fingerprint; + // Remove install addon since we've successfully installed it and + // added to main addon vec + install_addons.retain(|a| { + a.addon.as_ref().map(|a| &a.primary_folder_id) != Some(&id) + }); + } + }, + Err(error) => { + log::error!("{}", error); + ajour.error = Some(error.to_string()); - let mut version = None; - if let Some(package) = addon.relevant_release_package() { - version = Some(package.version.clone()); - } - if let Some(version) = version { - addon.set_version(version); + if reason == DownloadReason::Install { + if let Some(install_addon) = + install_addons.iter_mut().find(|a| a.id.to_string() == id) + { + install_addon.status = CatalogInstallStatus::Retry; } + } + } + } + + let mut commands = vec![]; + + if let (Some(addon), Some(folders)) = (addon, folders) { + addon.update_addon_folders(&folders); - if let Some(cache) = ajour.fingerprint_cache.as_ref() { - let mut commands = vec![]; + addon.state = AddonState::Fingerprint; - for folder in &addon.folders { + let mut version = None; + if let Some(package) = addon.relevant_release_package() { + version = Some(package.version.clone()); + } + if let Some(version) = version { + addon.set_version(version); + } + + // If we are updating / installing a Tukui / WowI + // addon, we want to update the cache. If we are installing a Curse + // addon, we want to make sure cache entry exists for those folders + if let Some(addon_cache) = &ajour.addon_cache { + if let Ok(entry) = AddonCacheEntry::try_from(addon as &_) { + match addon.active_repository { + // Remove any entry related to this cached addon + Some(Repository::Curse) => { commands.push(Command::perform( - perform_hash_addon( - reason, - ajour - .config - .get_addon_directory_for_flavor(&flavor) - .expect("Expected a valid path"), - folder.id.clone(), - cache.clone(), - flavor, - ), - Message::UpdateFingerprint, + remove_addon_cache_entry(addon_cache.clone(), entry, flavor), + Message::AddonCacheEntryRemoved, )); } - - return Ok(Command::batch(commands)); + // Update the entry for this cached addon + Some(Repository::Tukui) | Some(Repository::WowI) => { + commands.push(Command::perform( + update_addon_cache(addon_cache.clone(), entry, flavor), + Message::AddonCacheUpdated, + )); + } + None => {} } } - Err(error) => { - ajour.error = Some(error.to_string()); + } - // Update catalog status for addon - if reason == DownloadReason::Install { - update_catalog_install_status( - &mut ajour.catalog_install_statuses, - CatalogInstallStatus::Retry, + // Submit all addon folders to be fingerprinted + if let Some(cache) = ajour.fingerprint_cache.as_ref() { + for folder in &addon.folders { + commands.push(Command::perform( + perform_hash_addon( + ajour + .config + .get_addon_directory_for_flavor(&flavor) + .expect("Expected a valid path"), + folder.id.clone(), + cache.clone(), flavor, - addon.repository_id(), - ); - - remove_catalog_addon = Some(addon.primary_folder_id.clone()); - } + ), + Message::UpdateFingerprint, + )); } } } - // Remove catalog installed addon from addons since it failed - if let Some(id) = remove_catalog_addon { - addons.retain(|a| a.primary_folder_id != id) + if !commands.is_empty() { + return Ok(Command::batch(commands)); } } - Message::UpdateFingerprint((reason, flavor, id, result)) => { + Message::UpdateFingerprint((flavor, id, result)) => { log::debug!( "Message::UpdateFingerprint(({:?}, {}, error: {}))", flavor, @@ -722,63 +747,14 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { - // Add / update cache entry for addon if it's a Tukui - // or WowI addon - if addon.active_repository == Some(Repository::Tukui) - || addon.active_repository == Some(Repository::WowI) - { - return Ok(Command::perform( - update_addon_cache(addon_cache.clone(), entry, flavor), - Message::AddonCacheUpdated, - )); - } - } - Err(error) => { - log::error!("{}", error); - } - } - } } else { addon.state = AddonState::Ajour(Some("Error".to_owned())); - - // Update catalog status for addon - if reason == DownloadReason::Install { - update_catalog_install_status( - &mut ajour.catalog_install_statuses, - CatalogInstallStatus::Retry, - flavor, - addon.repository_id(), - ); - - remove_catalog_addon = Some(addon.primary_folder_id.clone()); - } } } - - // Remove catalog installed addon from addons since it failed - if let Some(id) = remove_catalog_addon { - addons.retain(|a| a.primary_folder_id != id) - } } Message::LatestRelease(release) => { log::debug!( @@ -1294,16 +1270,18 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result Result match result { - Ok(mut addon) => { - log::debug!( - "Message::CatalogInstallAddonFetched({:?}, {:?})", - flavor, - &id - ); + Message::CatalogInstallAddonFetched((flavor, id, result)) => { + let install_addons = ajour.catalog_install_addons.entry(flavor).or_default(); - if let Some(addons) = ajour.addons.get_mut(&flavor) { - // Add the addon to our collection - addon.state = AddonState::Downloading; - addons.push(addon.clone()); + if let Some(install_addon) = install_addons.iter_mut().find(|a| a.id == id) { + match result { + Ok(mut addon) => { + log::debug!( + "Message::CatalogInstallAddonFetched({:?}, {:?})", + flavor, + &id, + ); - let to_directory = ajour - .config - .get_download_directory_for_flavor(flavor) - .expect("Expected a valid path"); + addon.state = AddonState::Downloading; + install_addon.addon = Some(addon.clone()); - return Ok(Command::perform( - perform_download_addon( - DownloadReason::Install, - ajour.shared_client.clone(), - flavor, - addon, - to_directory, - ), - Message::DownloadedAddon, - )); - } - } - Err(error) => { - log::error!("{}", error); + let to_directory = ajour + .config + .get_download_directory_for_flavor(flavor) + .expect("Expected a valid path"); - update_catalog_install_status( - &mut ajour.catalog_install_statuses, - CatalogInstallStatus::Unavilable, - flavor, - Some(id.to_string()), - ); + return Ok(Command::perform( + perform_download_addon( + DownloadReason::Install, + ajour.shared_client.clone(), + flavor, + addon, + to_directory, + ), + Message::DownloadedAddon, + )); + } + Err(error) => { + log::error!("{}", error); + + install_addon.status = CatalogInstallStatus::Unavilable; + } + } } - }, + } Message::FetchedChangelog((addon, key, result)) => { log::debug!("Message::FetchedChangelog(error: {})", &result.is_err()); match result { @@ -1443,6 +1419,11 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { log::debug!("Message::AddonCacheUpdated({})", entry.title); } + Message::AddonCacheEntryRemoved(maybe_entry) => { + if let Some(entry) = maybe_entry { + log::debug!("Message::AddonCacheEntryRemoved({})", entry.title); + } + } Message::Error(error) | Message::CatalogDownloaded(Err(error)) | Message::AddonCacheUpdated(Err(error)) => { @@ -1543,14 +1524,12 @@ async fn perform_download_addon( /// Rehashes a `Addon`. async fn perform_hash_addon( - reason: DownloadReason, addon_dir: impl AsRef, addon_id: String, fingerprint_cache: Arc>, flavor: Flavor, -) -> (DownloadReason, Flavor, String, Result<()>) { +) -> (Flavor, String, Result<()>) { ( - reason, flavor, addon_id.clone(), update_addon_fingerprint(fingerprint_cache, flavor, addon_dir, addon_id).await, @@ -1835,20 +1814,6 @@ fn save_column_configs(ajour: &mut Ajour) { let _ = ajour.config.save(); } -fn update_catalog_install_status( - statuses: &mut Vec<(Flavor, u32, CatalogInstallStatus)>, - new_status: CatalogInstallStatus, - flavor: Flavor, - repository_id: Option, -) { - if let Some((_, _, status)) = statuses - .iter_mut() - .find(|(f, i, _)| flavor == *f && repository_id == Some(i.to_string())) - { - *status = new_status; - } -} - /// Hardcoded binary names for each compilation target /// that gets published to the Github Release const fn bin_name() -> &'static str {