feat: added support for github hosted bundle mods (#883)
* feat: added support for github hosted bundle mods * fix: keep external bundle mods when their manifest entry gains an id * fix: match a pending external bundle update to its installed jar * chore: parse the bundle file host with url instead of by hand * fix: namespace external bundle file ids under ext: * chore: rename the Browser tab to Online and list unbundled GitHub mods there --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
parent
abc452f294
commit
b3b436bc3e
17 changed files with 484 additions and 216 deletions
1
packages/oneclient_app/assets/icons/github.svg
Normal file
1
packages/oneclient_app/assets/icons/github.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 678 B |
|
|
@ -110,7 +110,11 @@ impl Component for ClusterUpdatePopup {
|
|||
})
|
||||
.collect();
|
||||
let mut meta = MetaMap::new();
|
||||
for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() {
|
||||
for provider in ProviderId::REMOTE_PROVIDERS
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([ProviderId::Local])
|
||||
{
|
||||
let ids: Vec<String> = all_items
|
||||
.iter()
|
||||
.filter(|i| i.provider == provider)
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ pub enum IconType {
|
|||
Folder,
|
||||
FolderCheck,
|
||||
FolderDownload,
|
||||
Github,
|
||||
Globe01,
|
||||
HelpCircle,
|
||||
IconLogo,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,9 @@ pub use optional_mods_popup::OptionalModsPopup;
|
|||
pub use overlay_popup::{OVERLAY_BASE_LEVEL, OVERLAY_MAX_LEVEL, OverlayPopup, overlay_is_topmost};
|
||||
pub use package_delete_dialog::use_shared_delete;
|
||||
pub(crate) use package_row::{CARD_GRID_H, GRID_GAP, GRID_MIN_W, grid_card, package_icon};
|
||||
pub use package_row::{CardLayout, PackageEntry, PackageRow, package_context_menu, provider_badge};
|
||||
pub use package_row::{
|
||||
CardLayout, PackageEntry, PackageRow, github_badge, package_context_menu, provider_badge,
|
||||
};
|
||||
pub use package_update_popup::PackageUpdatePopup;
|
||||
pub use pagination::Pagination;
|
||||
pub use player_model::PlayerModel;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ impl Component for OptionalModsPopup {
|
|||
let groups = snapshot.optional_mods.clone();
|
||||
|
||||
let mut meta = MetaMap::new();
|
||||
for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() {
|
||||
for provider in ProviderId::REMOTE_PROVIDERS
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([ProviderId::Local])
|
||||
{
|
||||
let ids: Vec<String> = groups
|
||||
.iter()
|
||||
.flatten()
|
||||
|
|
@ -100,6 +104,7 @@ fn entry_from_item(
|
|||
package_id,
|
||||
bundle_name: item.offer.as_ref().map(|(bundle, _)| bundle.clone()),
|
||||
provider: item.provider,
|
||||
github_hosted: item.github_hosted,
|
||||
name: cached
|
||||
.map(|cached| cached.name.clone())
|
||||
.filter(|name| !name.is_empty())
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ pub struct PackageEntry {
|
|||
pub package_id: String,
|
||||
pub bundle_name: Option<String>,
|
||||
pub provider: ProviderId,
|
||||
pub github_hosted: bool,
|
||||
pub name: String,
|
||||
pub file_name: String,
|
||||
pub author: String,
|
||||
|
|
@ -398,7 +399,9 @@ fn grid_meta(
|
|||
) -> Element {
|
||||
let muted = CARD_NAME.with_a(scale_a(alpha, 0.5));
|
||||
|
||||
let source = if item.is_remote() && navigable {
|
||||
let source = if item.github_hosted {
|
||||
meta_text("GitHub".to_string(), muted)
|
||||
} else if item.is_remote() && navigable {
|
||||
SourceLink {
|
||||
provider: item.provider,
|
||||
package_id: item.package_id.clone(),
|
||||
|
|
@ -615,7 +618,9 @@ fn package_info(
|
|||
.max_width(Size::percent(60.))
|
||||
.color(CARD_NAME),
|
||||
)
|
||||
.child(if remote {
|
||||
.child(if item.github_hosted {
|
||||
github_badge()
|
||||
} else if remote {
|
||||
provider_badge(item.provider)
|
||||
} else {
|
||||
local_badge()
|
||||
|
|
@ -704,6 +709,13 @@ pub fn provider_badge(provider: ProviderId) -> Element {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn github_badge() -> Element {
|
||||
badge(
|
||||
Icon::new(IconType::Github).size(12.).into_element(),
|
||||
"GitHub".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn outdated_badge() -> Element {
|
||||
accent_badge(
|
||||
Icon::new(IconType::RefreshCw01)
|
||||
|
|
|
|||
|
|
@ -46,14 +46,16 @@ pub fn item_from_bundle_file(file: &oneclient_core::BundleFile) -> ClusterUpdate
|
|||
..
|
||||
} => ClusterUpdateItem {
|
||||
provider: *provider,
|
||||
github_hosted: false,
|
||||
project_id: Some(project_id.clone()),
|
||||
fallback: file.display_name(),
|
||||
offer: None,
|
||||
status: None,
|
||||
},
|
||||
oneclient_core::BundleFileKind::External(_) => ClusterUpdateItem {
|
||||
oneclient_core::BundleFileKind::External { .. } => ClusterUpdateItem {
|
||||
provider: oneclient_content::packages::ProviderId::Local,
|
||||
project_id: None,
|
||||
github_hosted: file.is_github_hosted(),
|
||||
project_id: Some(file.kind.metadata_id()),
|
||||
fallback: file.display_name(),
|
||||
offer: None,
|
||||
status: None,
|
||||
|
|
@ -90,6 +92,7 @@ async fn cluster_update_summary(
|
|||
provider: r
|
||||
.provider
|
||||
.unwrap_or(oneclient_content::packages::ProviderId::Local),
|
||||
github_hosted: false,
|
||||
project_id: r.project_id.clone(),
|
||||
fallback: r
|
||||
.display_name
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub enum OptionalModsOutcome {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ClusterUpdateItem {
|
||||
pub provider: ProviderId,
|
||||
pub github_hosted: bool,
|
||||
pub project_id: Option<String>,
|
||||
/// Used when the meta cache has no entry (file name / package id)
|
||||
pub fallback: String,
|
||||
|
|
@ -58,6 +59,7 @@ impl ClusterUpdateItem {
|
|||
pub fn from_name(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
provider: ProviderId::Local,
|
||||
github_hosted: false,
|
||||
project_id: None,
|
||||
fallback: name.into(),
|
||||
offer: None,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ use std::collections::{HashMap, HashSet};
|
|||
use freya::prelude::*;
|
||||
use oneclient_common::search::{MatchScore, SearchQuery};
|
||||
use oneclient_content::packages::{CachedPackageMeta, ContentType, ProviderId};
|
||||
use oneclient_core::{BundleFileKind, BundleWithUpdateStatus, LinkedArtifactInfo};
|
||||
use oneclient_core::{
|
||||
BundleFileKind, BundleWithUpdateStatus, FileUpdateStatus, LinkedArtifactInfo,
|
||||
};
|
||||
use oneclient_db::models::OverrideType;
|
||||
|
||||
use crate::components::{CARD_GRID_H, CardLayout, GRID_GAP, GRID_MIN_W, PackageEntry};
|
||||
|
|
@ -52,11 +54,22 @@ fn provider_project_ids(
|
|||
ids
|
||||
}
|
||||
|
||||
fn local_project_ids(content: &[LinkedArtifactInfo], content_type: ContentType) -> Vec<String> {
|
||||
fn local_project_ids(
|
||||
content: &[LinkedArtifactInfo],
|
||||
bundles: &[BundleWithUpdateStatus],
|
||||
content_type: ContentType,
|
||||
) -> Vec<String> {
|
||||
let bundled = bundles
|
||||
.iter()
|
||||
.flat_map(|bundle| &bundle.files)
|
||||
.filter(|(file, _status)| file.content_type() == content_type)
|
||||
.filter(|(file, _status)| matches!(file.kind, BundleFileKind::External { .. }))
|
||||
.map(|(file, _status)| file.kind.metadata_id());
|
||||
content
|
||||
.iter()
|
||||
.filter(|info| info.content_type == content_type && info.provider.is_none())
|
||||
.map(|info| info.hash.clone())
|
||||
.chain(bundled)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +87,10 @@ pub fn use_content_meta(
|
|||
}
|
||||
}
|
||||
|
||||
let local = use_package_meta_batch(ProviderId::Local, local_project_ids(content, content_type));
|
||||
let local = use_package_meta_batch(
|
||||
ProviderId::Local,
|
||||
local_project_ids(content, bundles, content_type),
|
||||
);
|
||||
for (hash, meta) in package_meta_batch(&local) {
|
||||
out.insert((ProviderId::Local, hash), meta);
|
||||
}
|
||||
|
|
@ -117,7 +133,7 @@ pub fn bundle_packages(
|
|||
for bundle in bundles {
|
||||
let bundle_name = &bundle.archive.manifest.name;
|
||||
let category = bundle.archive.manifest.category.clone();
|
||||
for (file, _status) in &bundle.files {
|
||||
for (file, status) in &bundle.files {
|
||||
if file.content_type() != content_type {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -130,14 +146,24 @@ pub fn bundle_packages(
|
|||
continue;
|
||||
}
|
||||
|
||||
let provider = match &file.kind {
|
||||
BundleFileKind::Managed { provider, .. } => *provider,
|
||||
BundleFileKind::External(_) => ProviderId::Local,
|
||||
};
|
||||
let installed_info = by_project
|
||||
.get(pid.as_str())
|
||||
.or_else(|| by_hash.get(pid.as_str()))
|
||||
.copied();
|
||||
let provider = file.kind.metadata_provider();
|
||||
let installed_info = match &file.kind {
|
||||
BundleFileKind::Managed { .. } => by_project
|
||||
.get(pid.as_str())
|
||||
.or_else(|| by_hash.get(pid.as_str())),
|
||||
BundleFileKind::External { file: ext, .. } => {
|
||||
let hash = match status {
|
||||
FileUpdateStatus::UpdateAvailable {
|
||||
installed_version_id,
|
||||
..
|
||||
} => installed_version_id,
|
||||
_ => &ext.sha1,
|
||||
};
|
||||
seen.insert(hash.clone());
|
||||
by_hash.get(hash.as_str())
|
||||
}
|
||||
}
|
||||
.copied();
|
||||
let ov = overrides
|
||||
.get(&(bundle_name.clone(), pid.clone()))
|
||||
.map(String::as_str);
|
||||
|
|
@ -160,8 +186,9 @@ pub fn bundle_packages(
|
|||
categories,
|
||||
enabled,
|
||||
file.enabled,
|
||||
file.is_github_hosted(),
|
||||
installed_info,
|
||||
meta,
|
||||
meta.get(&(provider, file.kind.metadata_id())),
|
||||
file.display_name(),
|
||||
false,
|
||||
// Flagged rather than dropped `HiddenFilter` filters on the row and the seen id stops the loose-content pass resurrecting it as a local file
|
||||
|
|
@ -179,6 +206,7 @@ pub fn bundle_packages(
|
|||
let provider = info.provider.unwrap_or(ProviderId::Local);
|
||||
let pid = info.project_id.clone().unwrap_or_else(|| info.hash.clone());
|
||||
let outdated = stale.contains(&info.hash);
|
||||
let row_meta = meta.get(&(provider, pid.clone()));
|
||||
rows.push(make_row(
|
||||
pid,
|
||||
None,
|
||||
|
|
@ -187,8 +215,9 @@ pub fn bundle_packages(
|
|||
Vec::new(),
|
||||
info.enabled,
|
||||
true,
|
||||
false,
|
||||
Some(info),
|
||||
meta,
|
||||
row_meta,
|
||||
info.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| info.file_name.clone()),
|
||||
|
|
@ -209,13 +238,13 @@ fn make_row(
|
|||
categories: Vec<String>,
|
||||
enabled: bool,
|
||||
manifest_default: bool,
|
||||
github_hosted: bool,
|
||||
installed_info: Option<&LinkedArtifactInfo>,
|
||||
meta: &PackageMetaMap,
|
||||
m: Option<&CachedPackageMeta>,
|
||||
fallback_name: String,
|
||||
update_available: bool,
|
||||
hidden: bool,
|
||||
) -> PackageEntry {
|
||||
let m = meta.get(&(provider, package_id.clone()));
|
||||
let name = m
|
||||
.map(|p| p.name.clone())
|
||||
.filter(|s| !s.is_empty())
|
||||
|
|
@ -237,6 +266,7 @@ fn make_row(
|
|||
package_id,
|
||||
bundle_name,
|
||||
provider,
|
||||
github_hosted,
|
||||
name,
|
||||
file_name,
|
||||
author,
|
||||
|
|
@ -268,7 +298,7 @@ impl Tab {
|
|||
match self {
|
||||
Tab::All => "All".to_string(),
|
||||
Tab::Category(c) => c.clone(),
|
||||
Tab::Browser => "Browser".to_string(),
|
||||
Tab::Browser => "Online".to_string(),
|
||||
Tab::Local => "Local".to_string(),
|
||||
}
|
||||
}
|
||||
|
|
@ -277,8 +307,8 @@ impl Tab {
|
|||
match self {
|
||||
Tab::All => true,
|
||||
Tab::Category(c) => p.categories.iter().any(|pc| pc == c),
|
||||
Tab::Browser => p.is_remote() && !p.in_bundle(),
|
||||
Tab::Local => !p.is_remote(),
|
||||
Tab::Browser => (p.is_remote() || p.github_hosted) && !p.in_bundle(),
|
||||
Tab::Local => !p.is_remote() && !p.github_hosted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -331,7 +361,7 @@ fn build_tabs(categories: &[String], items: &[PackageEntry], hidden: HiddenFilte
|
|||
}
|
||||
}
|
||||
|
||||
// Category tabs are hidden when empty All + Browser + Local are always shown
|
||||
// Category tabs are hidden when empty All + Online + Local are always shown
|
||||
let mut tabs: Vec<Tab> = vec![Tab::All];
|
||||
tabs.extend(
|
||||
cats.into_iter()
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use std::collections::{HashMap, HashSet};
|
|||
|
||||
use freya::prelude::*;
|
||||
use oneclient_content::packages::ProviderId;
|
||||
use oneclient_core::BundleArchive;
|
||||
use oneclient_core::clusters::Cluster;
|
||||
use oneclient_core::{BundleArchive, BundleFile, BundleFileKind};
|
||||
|
||||
use crate::components::ScrollArea;
|
||||
use crate::hooks::{
|
||||
|
|
@ -67,13 +67,6 @@ fn fps_warning_banner() -> impl IntoElement {
|
|||
.child(text)
|
||||
}
|
||||
|
||||
fn file_provider(file: &BundleFile) -> ProviderId {
|
||||
match &file.kind {
|
||||
BundleFileKind::Managed { provider, .. } => *provider,
|
||||
BundleFileKind::External(_) => ProviderId::Local,
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_display_name(archive: &BundleArchive) -> String {
|
||||
let category = archive.manifest.category.trim();
|
||||
if category.is_empty() {
|
||||
|
|
@ -149,7 +142,9 @@ fn opt_in_bundles(clusters: &[ClusterBundles]) -> Vec<OptInBundle> {
|
|||
#[derive(Clone)]
|
||||
struct OptionalMod {
|
||||
package_id: String,
|
||||
metadata_id: String,
|
||||
provider: ProviderId,
|
||||
github_hosted: bool,
|
||||
fallback_name: String,
|
||||
size: u64,
|
||||
/// `(cluster_id, bundle_name)` pairs this mod is available from
|
||||
|
|
@ -186,7 +181,9 @@ fn optional_mods(clusters: &[ClusterBundles]) -> Vec<OptionalMod> {
|
|||
order.push(package_id.clone());
|
||||
OptionalMod {
|
||||
package_id: package_id.clone(),
|
||||
provider: file_provider(file),
|
||||
metadata_id: file.kind.metadata_id(),
|
||||
provider: file.kind.metadata_provider(),
|
||||
github_hosted: file.is_github_hosted(),
|
||||
fallback_name: file.display_name(),
|
||||
size: file.size,
|
||||
locations: Vec::new(),
|
||||
|
|
@ -259,9 +256,10 @@ impl Component for OnboardingBundles {
|
|||
let extras = optional_mods(&clusters);
|
||||
|
||||
// Hooks so they run every render regardless of whether there are extras yet
|
||||
let (mr_ids, cf_ids) = collect_ids(&extras);
|
||||
let (mr_ids, cf_ids, local_ids) = collect_ids(&extras);
|
||||
let mr_meta = package_meta_batch(&use_package_meta_batch(ProviderId::Modrinth, mr_ids));
|
||||
let cf_meta = package_meta_batch(&use_package_meta_batch(ProviderId::CurseForge, cf_ids));
|
||||
let local_meta = package_meta_batch(&use_package_meta_batch(ProviderId::Local, local_ids));
|
||||
|
||||
let mut sections: Vec<Element> = Vec::new();
|
||||
|
||||
|
|
@ -278,6 +276,7 @@ impl Component for OnboardingBundles {
|
|||
user_touched,
|
||||
&mr_meta,
|
||||
&cf_meta,
|
||||
&local_meta,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -441,14 +440,15 @@ fn extras_section(
|
|||
user_touched: State<bool>,
|
||||
mr_meta: &MetaMap,
|
||||
cf_meta: &MetaMap,
|
||||
local_meta: &MetaMap,
|
||||
) -> Element {
|
||||
let cards: Vec<Element> = extras
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let meta = match entry.provider {
|
||||
ProviderId::Modrinth => mr_meta.get(&entry.package_id),
|
||||
ProviderId::CurseForge => cf_meta.get(&entry.package_id),
|
||||
ProviderId::Local => None,
|
||||
ProviderId::Modrinth => mr_meta.get(&entry.metadata_id),
|
||||
ProviderId::CurseForge => cf_meta.get(&entry.metadata_id),
|
||||
ProviderId::Local => local_meta.get(&entry.metadata_id),
|
||||
};
|
||||
let name = meta
|
||||
.map(|m| m.name.clone())
|
||||
|
|
@ -461,6 +461,7 @@ fn extras_section(
|
|||
|
||||
OnboardingModCard {
|
||||
provider: entry.provider,
|
||||
github_hosted: entry.github_hosted,
|
||||
name,
|
||||
author,
|
||||
description,
|
||||
|
|
@ -518,15 +519,16 @@ fn extras_section(
|
|||
.into_element()
|
||||
}
|
||||
|
||||
fn collect_ids(extras: &[OptionalMod]) -> (Vec<String>, Vec<String>) {
|
||||
fn collect_ids(extras: &[OptionalMod]) -> (Vec<String>, Vec<String>, Vec<String>) {
|
||||
let mut mr = Vec::new();
|
||||
let mut cf = Vec::new();
|
||||
let mut local = Vec::new();
|
||||
for entry in extras {
|
||||
match entry.provider {
|
||||
ProviderId::Modrinth => mr.push(entry.package_id.clone()),
|
||||
ProviderId::CurseForge => cf.push(entry.package_id.clone()),
|
||||
ProviderId::Local => {}
|
||||
ProviderId::Modrinth => mr.push(entry.metadata_id.clone()),
|
||||
ProviderId::CurseForge => cf.push(entry.metadata_id.clone()),
|
||||
ProviderId::Local => local.push(entry.metadata_id.clone()),
|
||||
}
|
||||
}
|
||||
(mr, cf)
|
||||
(mr, cf, local)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use freya::prelude::*;
|
||||
use oneclient_content::packages::ProviderId;
|
||||
|
||||
use crate::components::{Icon, provider_badge};
|
||||
use crate::components::{Icon, IconType, github_badge, provider_badge};
|
||||
use crate::hooks::{loaded_image, use_cached_image};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{ImageFallbackExt, border_all_color};
|
||||
|
|
@ -16,6 +16,7 @@ pub(super) const GRID_GAP: f32 = 10.;
|
|||
#[derive(PartialEq)]
|
||||
pub(super) struct OnboardingModCard {
|
||||
pub(super) provider: ProviderId,
|
||||
pub(super) github_hosted: bool,
|
||||
pub(super) name: String,
|
||||
pub(super) author: String,
|
||||
pub(super) description: String,
|
||||
|
|
@ -36,10 +37,10 @@ impl Component for OnboardingModCard {
|
|||
.height(Size::px(44.))
|
||||
.aspect_ratio(AspectRatio::Min)
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.fallback(icon_box(self.provider))
|
||||
.fallback(icon_box(self.placeholder_icon()))
|
||||
.into_element(),
|
||||
|
||||
None => icon_box(self.provider).into_element(),
|
||||
None => icon_box(self.placeholder_icon()).into_element(),
|
||||
};
|
||||
|
||||
let (bg, border, alpha) = if self.enabled {
|
||||
|
|
@ -82,7 +83,11 @@ impl Component for OnboardingModCard {
|
|||
.color(colors::fg_secondary().with_a(alpha)),
|
||||
)
|
||||
})
|
||||
.child(provider_badge(self.provider)),
|
||||
.child(if self.github_hosted {
|
||||
github_badge()
|
||||
} else {
|
||||
provider_badge(self.provider)
|
||||
}),
|
||||
);
|
||||
|
||||
let description = (!self.description.is_empty()).then(|| {
|
||||
|
|
@ -132,14 +137,24 @@ impl Component for OnboardingModCard {
|
|||
}
|
||||
}
|
||||
|
||||
fn icon_box(provider: ProviderId) -> impl IntoElement {
|
||||
impl OnboardingModCard {
|
||||
fn placeholder_icon(&self) -> IconType {
|
||||
if self.github_hosted {
|
||||
IconType::Github
|
||||
} else {
|
||||
self.provider.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_box(icon: IconType) -> impl IntoElement {
|
||||
rect()
|
||||
.center()
|
||||
.width(Size::px(44.))
|
||||
.height(Size::px(44.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(colors::component_bg())
|
||||
.child(Icon::new(provider).size(20.).color(colors::fg_secondary()))
|
||||
.child(Icon::new(icon).size(20.).color(colors::fg_secondary()))
|
||||
.into_element()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,17 @@ pub fn file(package_id: &str, enabled: bool, hidden: bool) -> BundleFile {
|
|||
hidden,
|
||||
path: format!("mods/{package_id}.jar"),
|
||||
size: 1,
|
||||
kind: BundleFileKind::External(ExternalFile {
|
||||
name: format!("{package_id}.jar"),
|
||||
url: format!("https://example.invalid/{package_id}.jar"),
|
||||
sha1: package_id.to_string(),
|
||||
size: 1,
|
||||
content_type: ContentType::Mod,
|
||||
}),
|
||||
kind: BundleFileKind::External {
|
||||
file: ExternalFile {
|
||||
name: format!("{package_id}.jar"),
|
||||
url: format!("https://example.invalid/{package_id}.jar"),
|
||||
sha1: package_id.to_string(),
|
||||
size: 1,
|
||||
content_type: ContentType::Mod,
|
||||
},
|
||||
id: None,
|
||||
meta: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ pub async fn install_package_from_bundle(
|
|||
.await?;
|
||||
artifact.hash
|
||||
}
|
||||
BundleFileKind::External(ext) => {
|
||||
BundleFileKind::External { file: ext, .. } => {
|
||||
install_external(ext, &cluster, skip_compatibility, child, ctx).await?
|
||||
}
|
||||
};
|
||||
|
|
@ -239,6 +239,74 @@ async fn clear_reconciler_disables(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn external_ids_by_sha1(
|
||||
archives: &[BundleArchive],
|
||||
) -> std::collections::HashMap<String, String> {
|
||||
archives
|
||||
.iter()
|
||||
.flat_map(|archive| &archive.manifest.files)
|
||||
.filter_map(|file| match &file.kind {
|
||||
BundleFileKind::External {
|
||||
file: ext,
|
||||
id: Some(id),
|
||||
..
|
||||
} => Some((ext.sha1.clone(), id.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn adopt_external_ids(
|
||||
cluster_id: i64,
|
||||
archives: &[BundleArchive],
|
||||
ctx: &ContentCtx,
|
||||
) -> ContentResult<()> {
|
||||
let ids = external_ids_by_sha1(archives);
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for row in bundle_dao::list_bundle_tracked(&ctx.db, cluster_id).await? {
|
||||
let (Some(bundle_name), Some(version_id), Some(package_id)) =
|
||||
(&row.bundle_name, &row.bundle_version_id, &row.package_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if *package_id != row.hash {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = ids.get(package_id) else {
|
||||
continue;
|
||||
};
|
||||
bundle_dao::track_bundle_artifact(
|
||||
&ctx.db,
|
||||
cluster_id,
|
||||
&row.hash,
|
||||
bundle_name,
|
||||
version_id,
|
||||
id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
||||
for row in &overrides {
|
||||
let Some(id) = ids.get(&row.package_id) else {
|
||||
continue;
|
||||
};
|
||||
let superseded = overrides
|
||||
.iter()
|
||||
.any(|other| other.bundle_name == row.bundle_name && other.package_id == *id);
|
||||
if !superseded && let Some(override_type) = OverrideType::parse(&row.override_type) {
|
||||
bundle_dao::save_override(&ctx.db, cluster_id, &row.bundle_name, id, override_type)
|
||||
.await?;
|
||||
}
|
||||
bundle_dao::remove_override(&ctx.db, cluster_id, &row.bundle_name, &row.package_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(archives, ctx))]
|
||||
pub async fn heal_bundle_activity(
|
||||
cluster_id: i64,
|
||||
|
|
@ -246,6 +314,7 @@ pub async fn heal_bundle_activity(
|
|||
ctx: &ContentCtx,
|
||||
) -> ContentResult<()> {
|
||||
clear_reconciler_disables(cluster_id, archives, ctx).await?;
|
||||
adopt_external_ids(cluster_id, archives, ctx).await?;
|
||||
|
||||
let tracked = bundle_dao::list_bundle_tracked(&ctx.db, cluster_id).await?;
|
||||
if tracked.iter().all(|row| row.enabled != 0) {
|
||||
|
|
@ -332,18 +401,10 @@ pub async fn install_enabled_bundle_files(
|
|||
let bundle_name = archive.manifest.name.clone();
|
||||
let mut installed = Vec::new();
|
||||
|
||||
let linked = PackageStore::list_linked_artifacts(cluster_id, ctx).await?;
|
||||
let mut linked_projects: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
let mut linked_hashes: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
// "Already installed" means the database not disk
|
||||
// content lives in the cache between sessions so probing the folder would
|
||||
// reinstall everything
|
||||
for info in &linked {
|
||||
if let Some(pid) = &info.project_id {
|
||||
linked_projects.insert(pid.as_str());
|
||||
}
|
||||
linked_hashes.insert(info.hash.as_str());
|
||||
}
|
||||
let present = PresentContent::load(cluster_id, ctx).await?;
|
||||
|
||||
let to_install: Vec<BundleFile> = archive
|
||||
.manifest
|
||||
|
|
@ -351,16 +412,8 @@ pub async fn install_enabled_bundle_files(
|
|||
.iter()
|
||||
.filter(|file| {
|
||||
let package_id = file.kind.package_id();
|
||||
if !effective_enabled(file, find_override(&overrides, &bundle_name, &package_id)) {
|
||||
return false;
|
||||
}
|
||||
let already_installed = match &file.kind {
|
||||
BundleFileKind::Managed { project_id, .. } => {
|
||||
linked_projects.contains(project_id.as_str())
|
||||
}
|
||||
BundleFileKind::External(ext) => linked_hashes.contains(ext.sha1.as_str()),
|
||||
};
|
||||
!already_installed
|
||||
effective_enabled(file, find_override(&overrides, &bundle_name, &package_id))
|
||||
&& !present.contains(file)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
|
@ -425,6 +478,42 @@ pub async fn install_enabled_bundle_files(
|
|||
Ok(installed)
|
||||
}
|
||||
|
||||
struct PresentContent {
|
||||
projects: std::collections::HashSet<String>,
|
||||
hashes: std::collections::HashSet<String>,
|
||||
tracked_ids: std::collections::HashSet<String>,
|
||||
}
|
||||
|
||||
impl PresentContent {
|
||||
async fn load(cluster_id: i64, ctx: &ContentCtx) -> ContentResult<Self> {
|
||||
let linked = PackageStore::list_linked_artifacts(cluster_id, ctx).await?;
|
||||
let tracked_ids = bundle_dao::list_bundle_tracked(&ctx.db, cluster_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|row| row.package_id)
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
projects: linked
|
||||
.iter()
|
||||
.filter_map(|info| info.project_id.clone())
|
||||
.collect(),
|
||||
hashes: linked.into_iter().map(|info| info.hash).collect(),
|
||||
tracked_ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn contains(&self, file: &BundleFile) -> bool {
|
||||
match &file.kind {
|
||||
BundleFileKind::Managed { project_id, .. } => self.projects.contains(project_id),
|
||||
BundleFileKind::External { file: ext, id, .. } => {
|
||||
self.hashes.contains(&ext.sha1)
|
||||
|| id.as_ref().is_some_and(|id| self.tracked_ids.contains(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kept modest each fetch also costs a provider API call rate limited per-minute
|
||||
pub(crate) const BUNDLE_INSTALL_CONCURRENCY: usize = 6;
|
||||
|
||||
|
|
@ -445,32 +534,16 @@ pub async fn enabled_bundle_bytes(
|
|||
.archives_for(ctx, &cluster.mc_version, loader)
|
||||
.await?;
|
||||
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
||||
let linked = PackageStore::list_linked_artifacts(cluster_id, ctx).await?;
|
||||
|
||||
let mut linked_projects: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
let mut linked_hashes: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
for info in &linked {
|
||||
if let Some(pid) = &info.project_id {
|
||||
linked_projects.insert(pid.as_str());
|
||||
}
|
||||
linked_hashes.insert(info.hash.as_str());
|
||||
}
|
||||
let present = PresentContent::load(cluster_id, ctx).await?;
|
||||
|
||||
let mut total = 0u64;
|
||||
for archive in &archives {
|
||||
let bundle_name = &archive.manifest.name;
|
||||
for file in &archive.manifest.files {
|
||||
let package_id = file.kind.package_id();
|
||||
if !effective_enabled(file, find_override(&overrides, bundle_name, &package_id)) {
|
||||
continue;
|
||||
}
|
||||
let already_installed = match &file.kind {
|
||||
BundleFileKind::Managed { project_id, .. } => {
|
||||
linked_projects.contains(project_id.as_str())
|
||||
}
|
||||
BundleFileKind::External(ext) => linked_hashes.contains(ext.sha1.as_str()),
|
||||
};
|
||||
if already_installed {
|
||||
if !effective_enabled(file, find_override(&overrides, bundle_name, &package_id))
|
||||
|| present.contains(file)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
total += file.size;
|
||||
|
|
@ -832,13 +905,17 @@ mod tests {
|
|||
hidden: false,
|
||||
path: "mods/example.jar".to_string(),
|
||||
size: 1,
|
||||
kind: BundleFileKind::External(ExternalFile {
|
||||
name: "example.jar".to_string(),
|
||||
url: "https://example.invalid/example.jar".to_string(),
|
||||
sha1: "abc123".to_string(),
|
||||
size: 1,
|
||||
content_type: ContentType::Mod,
|
||||
}),
|
||||
kind: BundleFileKind::External {
|
||||
file: ExternalFile {
|
||||
name: "example.jar".to_string(),
|
||||
url: "https://example.invalid/example.jar".to_string(),
|
||||
sha1: "abc123".to_string(),
|
||||
size: 1,
|
||||
content_type: ContentType::Mod,
|
||||
},
|
||||
id: None,
|
||||
meta: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ use std::str::FromStr;
|
|||
|
||||
use chrono::Utc;
|
||||
use oneclient_db::dao::bundle as bundle_dao;
|
||||
use oneclient_db::dao::package_metadata as meta_dao;
|
||||
use oneclient_db::models::{BundleRow, NewBundle};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::bundles::error::BundleError;
|
||||
use crate::bundles::manifest::{BundleManifest as RemoteBundleManifest, RemoteBundleRef};
|
||||
use crate::bundles::polymrpack;
|
||||
use crate::bundles::types::BundleArchive;
|
||||
use crate::bundles::types::{BundleArchive, BundleFileKind};
|
||||
use crate::ctx::ContentCtx;
|
||||
use crate::error::{ContentError, ContentResult};
|
||||
use oneclient_common::domain::GameLoader;
|
||||
use oneclient_common::domain::{GameLoader, ProviderId};
|
||||
use oneclient_common::paths;
|
||||
use oneclient_net::RequestError;
|
||||
use oneclient_net::{EtagPolicy, fetch_cached};
|
||||
|
|
@ -149,21 +150,23 @@ impl BundlesManager {
|
|||
) -> ContentResult<Vec<BundleArchive>> {
|
||||
let mut archives = Vec::new();
|
||||
for bundle in self.list_for(ctx, mc_version, loader).await? {
|
||||
let manifest = self.manifest_for_archive(&bundle.path).await?;
|
||||
let manifest = self.manifest_for_archive(ctx, &bundle.path).await?;
|
||||
archives.push(BundleArchive { bundle, manifest });
|
||||
}
|
||||
Ok(archives)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
#[tracing::instrument(level = "debug", skip(self, ctx))]
|
||||
async fn manifest_for_archive(
|
||||
&self,
|
||||
ctx: &ContentCtx,
|
||||
path: &Path,
|
||||
) -> ContentResult<crate::bundles::types::BundleManifest> {
|
||||
if let Some(manifest) = self.archive_cache.read().await.get(path) {
|
||||
return Ok(manifest.clone());
|
||||
}
|
||||
let manifest = polymrpack::read_manifest_from_archive(path).await?;
|
||||
record_external_metadata(&manifest, ctx).await;
|
||||
self.archive_cache
|
||||
.write()
|
||||
.await
|
||||
|
|
@ -270,6 +273,37 @@ impl Default for BundlesManager {
|
|||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(bundle = %manifest.name))]
|
||||
async fn record_external_metadata(
|
||||
manifest: &crate::bundles::types::BundleManifest,
|
||||
ctx: &ContentCtx,
|
||||
) {
|
||||
for bundle_file in &manifest.files {
|
||||
let BundleFileKind::External {
|
||||
file,
|
||||
meta: Some(meta),
|
||||
..
|
||||
} = &bundle_file.kind
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(err) = meta_dao::upsert_package_metadata(
|
||||
&ctx.db,
|
||||
ProviderId::Local as i64,
|
||||
&file.sha1,
|
||||
meta.name.as_deref().unwrap_or(&file.name),
|
||||
meta.description.as_deref().unwrap_or_default(),
|
||||
&meta.authors.join(", "),
|
||||
meta.icon_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(file = %file.name, error = %err, "failed to record bundle file metadata");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FetchedManifest {
|
||||
manifest: RemoteBundleManifest,
|
||||
changed: bool,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use serde::Deserialize;
|
|||
|
||||
use crate::bundles::error::BundleError;
|
||||
use crate::bundles::types::{
|
||||
BundleFile, BundleFileKind, BundleManifest, content_type_from_bundle_path,
|
||||
BundleFile, BundleFileKind, BundleManifest, ExternalFileMeta, content_type_from_bundle_path,
|
||||
};
|
||||
use crate::error::ContentResult;
|
||||
use crate::packages::types::ExternalFile;
|
||||
|
|
@ -49,6 +49,22 @@ struct PolyMrpackFile {
|
|||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<PolyMrpackFileOverrides>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct PolyMrpackFileOverrides {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub authors: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -165,12 +181,45 @@ fn parse_bundle_file(file: &PolyMrpackFile) -> Option<BundleFile> {
|
|||
hidden: file.hidden,
|
||||
path: file.path.clone(),
|
||||
size: file.file_size,
|
||||
kind: BundleFileKind::External(ExternalFile {
|
||||
name: file_name,
|
||||
url: download_url,
|
||||
sha1: file.hashes.sha1.to_ascii_lowercase(),
|
||||
size: file.file_size,
|
||||
content_type: content_type_from_bundle_path(&file.path),
|
||||
}),
|
||||
kind: BundleFileKind::External {
|
||||
file: ExternalFile {
|
||||
name: file_name,
|
||||
url: download_url,
|
||||
sha1: file.hashes.sha1.to_ascii_lowercase(),
|
||||
size: file.file_size,
|
||||
content_type: content_type_from_bundle_path(&file.path),
|
||||
},
|
||||
id: non_blank(file.id.as_deref()).map(|id| {
|
||||
if id.starts_with(EXTERNAL_ID_PREFIX) {
|
||||
id
|
||||
} else {
|
||||
format!("{EXTERNAL_ID_PREFIX}{id}")
|
||||
}
|
||||
}),
|
||||
meta: file.overrides.as_ref().and_then(external_meta),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const EXTERNAL_ID_PREFIX: &str = "ext:";
|
||||
|
||||
fn non_blank(value: Option<&str>) -> Option<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn external_meta(overrides: &PolyMrpackFileOverrides) -> Option<ExternalFileMeta> {
|
||||
let meta = ExternalFileMeta {
|
||||
name: non_blank(overrides.name.as_deref()),
|
||||
description: non_blank(overrides.description.as_deref()),
|
||||
authors: overrides
|
||||
.authors
|
||||
.iter()
|
||||
.filter_map(|author| non_blank(Some(author)))
|
||||
.collect(),
|
||||
icon_url: non_blank(overrides.icon.as_deref()),
|
||||
};
|
||||
(meta != ExternalFileMeta::default()).then_some(meta)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,19 +35,39 @@ pub struct BundleFile {
|
|||
|
||||
impl BundleFile {
|
||||
pub fn content_type(&self) -> ContentType {
|
||||
if let BundleFileKind::External(ext) = &self.kind {
|
||||
return ext.content_type;
|
||||
if let BundleFileKind::External { file, .. } = &self.kind {
|
||||
return file.content_type;
|
||||
}
|
||||
content_type_from_bundle_path(&self.path)
|
||||
}
|
||||
|
||||
pub fn is_github_hosted(&self) -> bool {
|
||||
let BundleFileKind::External { file, .. } = &self.kind else {
|
||||
return false;
|
||||
};
|
||||
url::Url::parse(&file.url).is_ok_and(|url| {
|
||||
url.host_str().is_some_and(|host| {
|
||||
host == "github.com"
|
||||
|| host.ends_with(".github.com")
|
||||
|| host.ends_with(".githubusercontent.com")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> String {
|
||||
if let BundleFileKind::External {
|
||||
meta: Some(meta), ..
|
||||
} = &self.kind
|
||||
&& let Some(name) = &meta.name
|
||||
{
|
||||
return name.clone();
|
||||
}
|
||||
let from_path = self.path.rsplit('/').next().filter(|s| !s.is_empty());
|
||||
if let Some(name) = from_path {
|
||||
return name.to_string();
|
||||
}
|
||||
match &self.kind {
|
||||
BundleFileKind::External(ext) => ext.name.clone(),
|
||||
BundleFileKind::External { file, .. } => file.name.clone(),
|
||||
BundleFileKind::Managed { project_id, .. } => project_id.clone(),
|
||||
}
|
||||
}
|
||||
|
|
@ -61,21 +81,58 @@ pub enum BundleFileKind {
|
|||
version_id: String,
|
||||
sha1: String,
|
||||
},
|
||||
External(ExternalFile),
|
||||
External {
|
||||
file: ExternalFile,
|
||||
id: Option<String>,
|
||||
meta: Option<ExternalFileMeta>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExternalFileMeta {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub authors: Vec<String>,
|
||||
pub icon_url: Option<String>,
|
||||
}
|
||||
|
||||
impl BundleFileKind {
|
||||
pub fn package_id(&self) -> String {
|
||||
match self {
|
||||
Self::Managed { project_id, .. } => project_id.clone(),
|
||||
Self::External(ext) => ext.sha1.clone(),
|
||||
Self::External { file, id, .. } => id.clone().unwrap_or_else(|| file.sha1.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bundle_version_id(&self) -> String {
|
||||
match self {
|
||||
Self::Managed { version_id, .. } => version_id.clone(),
|
||||
Self::External(ext) => ext.sha1.clone(),
|
||||
Self::External { file, .. } => file.sha1.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata_id(&self) -> String {
|
||||
match self {
|
||||
Self::Managed { project_id, .. } => project_id.clone(),
|
||||
Self::External { file, .. } => file.sha1.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata_provider(&self) -> ProviderId {
|
||||
match self {
|
||||
Self::Managed { provider, .. } => *provider,
|
||||
Self::External { .. } => ProviderId::Local,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bundle_key(&self) -> String {
|
||||
match self {
|
||||
Self::Managed {
|
||||
provider,
|
||||
project_id,
|
||||
..
|
||||
} => managed_bundle_key(*provider, project_id),
|
||||
Self::External { .. } => external_bundle_key(&self.package_id()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,8 +141,8 @@ pub fn managed_bundle_key(provider: ProviderId, package_id: &str) -> String {
|
|||
format!("m:{}:{package_id}", provider.dir_name())
|
||||
}
|
||||
|
||||
pub fn external_bundle_key(sha1: &str) -> String {
|
||||
format!("e:{sha1}")
|
||||
pub fn external_bundle_key(package_id: &str) -> String {
|
||||
format!("e:{package_id}")
|
||||
}
|
||||
|
||||
pub fn content_type_from_bundle_path(path: &str) -> ContentType {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ use tokio::sync::Mutex as AsyncMutex;
|
|||
use futures_util::StreamExt;
|
||||
|
||||
use crate::bundles::install::{
|
||||
BUNDLE_INSTALL_CONCURRENCY, disable_was_deliberate, find_user_suppression,
|
||||
heal_bundle_activity, install_package_from_bundle, remove_artifact_from_cluster,
|
||||
set_artifact_enabled_to,
|
||||
BUNDLE_INSTALL_CONCURRENCY, disable_was_deliberate, external_ids_by_sha1,
|
||||
find_user_suppression, heal_bundle_activity, install_package_from_bundle,
|
||||
remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||
};
|
||||
use crate::bundles::manager::BundlesManager;
|
||||
use crate::bundles::overrides;
|
||||
|
|
@ -144,14 +144,7 @@ async fn check_bundle_updates_inner(
|
|||
let mut shipped_keys = HashSet::new();
|
||||
|
||||
for file in &archive.manifest.files {
|
||||
shipped_keys.insert(match &file.kind {
|
||||
BundleFileKind::Managed {
|
||||
provider,
|
||||
project_id,
|
||||
..
|
||||
} => managed_bundle_key(*provider, project_id),
|
||||
BundleFileKind::External(ext) => external_bundle_key(&ext.sha1),
|
||||
});
|
||||
shipped_keys.insert(file.kind.bundle_key());
|
||||
|
||||
let user_override = overrides_map
|
||||
.get(&(archive.manifest.name.clone(), file.kind.package_id()))
|
||||
|
|
@ -159,26 +152,10 @@ async fn check_bundle_updates_inner(
|
|||
if !crate::bundles::effective_enabled(file, user_override) {
|
||||
continue;
|
||||
}
|
||||
match &file.kind {
|
||||
BundleFileKind::Managed {
|
||||
provider,
|
||||
project_id,
|
||||
version_id,
|
||||
..
|
||||
} => {
|
||||
let key = managed_bundle_key(*provider, project_id);
|
||||
files_map.insert(key.clone(), (version_id.clone(), file.clone()));
|
||||
if file.hidden {
|
||||
hidden_explicit_keys.insert(key);
|
||||
}
|
||||
}
|
||||
BundleFileKind::External(ext) => {
|
||||
let key = external_bundle_key(&ext.sha1);
|
||||
files_map.insert(key.clone(), (ext.sha1.clone(), file.clone()));
|
||||
if file.hidden {
|
||||
hidden_explicit_keys.insert(key);
|
||||
}
|
||||
}
|
||||
let key = file.kind.bundle_key();
|
||||
files_map.insert(key.clone(), (file.kind.bundle_version_id(), file.clone()));
|
||||
if file.hidden {
|
||||
hidden_explicit_keys.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +207,7 @@ async fn check_bundle_updates_inner(
|
|||
.map(|a| a.manifest.name.as_str())
|
||||
.collect();
|
||||
|
||||
let external_ids = external_ids_by_sha1(&archives);
|
||||
let mut updates_available = Vec::new();
|
||||
let mut removals_available = Vec::new();
|
||||
|
||||
|
|
@ -241,7 +219,8 @@ async fn check_bundle_updates_inner(
|
|||
continue;
|
||||
};
|
||||
|
||||
let installed_key = bundle_package_key(bundle_pkg, &all_linked_by_hash, pkg_id);
|
||||
let installed_key =
|
||||
bundle_package_key(bundle_pkg, &all_linked_by_hash, &external_ids, pkg_id);
|
||||
|
||||
let Some(bundle_name) = &bundle_pkg.bundle_name else {
|
||||
continue;
|
||||
|
|
@ -343,8 +322,15 @@ async fn check_bundle_updates_inner(
|
|||
let mut additions_available = Vec::new();
|
||||
let mut optional_available: Vec<(String, BundleOptionalPackage)> = Vec::new();
|
||||
let mut planned_addition_keys = all_installed_managed_keys.clone();
|
||||
for hash in all_installed_external_hashes {
|
||||
planned_addition_keys.insert(external_bundle_key(&hash));
|
||||
let tracked_package_by_hash: HashMap<&str, &str> = bundle_packages
|
||||
.iter()
|
||||
.filter_map(|bp| Some((bp.hash.as_str(), bp.package_id.as_deref()?)))
|
||||
.collect();
|
||||
for hash in &all_installed_external_hashes {
|
||||
planned_addition_keys.insert(external_bundle_key(hash));
|
||||
if let Some(package_id) = tracked_package_by_hash.get(hash.as_str()) {
|
||||
planned_addition_keys.insert(external_bundle_key(package_id));
|
||||
}
|
||||
}
|
||||
|
||||
for archive in &archives {
|
||||
|
|
@ -358,15 +344,13 @@ async fn check_bundle_updates_inner(
|
|||
.get(&(archive.manifest.name.clone(), file_id.clone()))
|
||||
.copied();
|
||||
|
||||
let file_key = match &file.kind {
|
||||
BundleFileKind::Managed {
|
||||
provider,
|
||||
project_id,
|
||||
..
|
||||
} => managed_bundle_key(*provider, project_id),
|
||||
BundleFileKind::External(ext) => external_bundle_key(&ext.sha1),
|
||||
};
|
||||
if planned_addition_keys.contains(&file_key) {
|
||||
let file_key = file.kind.bundle_key();
|
||||
let exact_file_installed = matches!(
|
||||
&file.kind,
|
||||
BundleFileKind::External { file: ext, .. }
|
||||
if planned_addition_keys.contains(&external_bundle_key(&ext.sha1))
|
||||
);
|
||||
if planned_addition_keys.contains(&file_key) || exact_file_installed {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -819,12 +803,17 @@ pub async fn get_bundles_with_update_status(
|
|||
let all_linked_by_hash: HashMap<String, &LinkedArtifactInfo> =
|
||||
all_linked.iter().map(|a| (a.hash.clone(), a)).collect();
|
||||
|
||||
let archives = bundles
|
||||
.archives_for(ctx, &cluster.mc_version, loader)
|
||||
.await?;
|
||||
let external_ids = external_ids_by_sha1(&archives);
|
||||
|
||||
let mut installed_map: HashMap<String, &BundleTrackedArtifactRow> = HashMap::new();
|
||||
for bundle_pkg in &bundle_packages {
|
||||
let Some(pkg_id) = &bundle_pkg.package_id else {
|
||||
continue;
|
||||
};
|
||||
let key = bundle_package_key(bundle_pkg, &all_linked_by_hash, pkg_id);
|
||||
let key = bundle_package_key(bundle_pkg, &all_linked_by_hash, &external_ids, pkg_id);
|
||||
installed_map.insert(key, bundle_pkg);
|
||||
}
|
||||
|
||||
|
|
@ -837,9 +826,6 @@ pub async fn get_bundles_with_update_status(
|
|||
})
|
||||
.collect();
|
||||
|
||||
let archives = bundles
|
||||
.archives_for(ctx, &cluster.mc_version, loader)
|
||||
.await?;
|
||||
let mut results = Vec::new();
|
||||
|
||||
for archive in archives {
|
||||
|
|
@ -847,48 +833,25 @@ pub async fn get_bundles_with_update_status(
|
|||
let mut has_updates = false;
|
||||
|
||||
for file in &archive.manifest.files {
|
||||
let status = match &file.kind {
|
||||
BundleFileKind::Managed {
|
||||
provider,
|
||||
project_id,
|
||||
version_id,
|
||||
..
|
||||
} => {
|
||||
let key = managed_bundle_key(*provider, project_id);
|
||||
if let Some(installed) = installed_map.get(&key) {
|
||||
let installed_version =
|
||||
installed.bundle_version_id.as_deref().unwrap_or("");
|
||||
if installed_version == version_id.as_str() {
|
||||
FileUpdateStatus::UpToDate
|
||||
} else {
|
||||
has_updates = true;
|
||||
FileUpdateStatus::UpdateAvailable {
|
||||
installed_version_id: installed_version.to_string(),
|
||||
new_version_id: version_id.clone(),
|
||||
}
|
||||
}
|
||||
} else if matches!(
|
||||
overrides_map.get(&(archive.manifest.name.clone(), project_id.clone())),
|
||||
Some(OverrideType::Removed)
|
||||
) {
|
||||
FileUpdateStatus::RemovedByUser
|
||||
} else {
|
||||
FileUpdateStatus::NotInstalled
|
||||
}
|
||||
}
|
||||
BundleFileKind::External(ext) => {
|
||||
let key = external_bundle_key(&ext.sha1);
|
||||
if installed_map.contains_key(&key) {
|
||||
FileUpdateStatus::UpToDate
|
||||
} else if matches!(
|
||||
overrides_map.get(&(archive.manifest.name.clone(), ext.sha1.clone())),
|
||||
Some(OverrideType::Removed)
|
||||
) {
|
||||
FileUpdateStatus::RemovedByUser
|
||||
} else {
|
||||
FileUpdateStatus::NotInstalled
|
||||
let new_version_id = file.kind.bundle_version_id();
|
||||
let status = if let Some(installed) = installed_map.get(&file.kind.bundle_key()) {
|
||||
let installed_version = installed.bundle_version_id.as_deref().unwrap_or("");
|
||||
if installed_version == new_version_id {
|
||||
FileUpdateStatus::UpToDate
|
||||
} else {
|
||||
has_updates = true;
|
||||
FileUpdateStatus::UpdateAvailable {
|
||||
installed_version_id: installed_version.to_string(),
|
||||
new_version_id,
|
||||
}
|
||||
}
|
||||
} else if matches!(
|
||||
overrides_map.get(&(archive.manifest.name.clone(), file.kind.package_id())),
|
||||
Some(OverrideType::Removed)
|
||||
) {
|
||||
FileUpdateStatus::RemovedByUser
|
||||
} else {
|
||||
FileUpdateStatus::NotInstalled
|
||||
};
|
||||
files.push((file.clone(), status));
|
||||
}
|
||||
|
|
@ -906,14 +869,21 @@ pub async fn get_bundles_with_update_status(
|
|||
fn bundle_package_key(
|
||||
bundle_pkg: &BundleTrackedArtifactRow,
|
||||
linked: &HashMap<String, &LinkedArtifactInfo>,
|
||||
external_ids: &HashMap<String, String>,
|
||||
package_id: &str,
|
||||
) -> String {
|
||||
if package_id == bundle_pkg.hash {
|
||||
external_bundle_key(&bundle_pkg.hash)
|
||||
} else if let Some(info) = linked.get(&bundle_pkg.hash) {
|
||||
managed_bundle_key(info.provider.unwrap_or(ProviderId::Modrinth), package_id)
|
||||
return external_bundle_key(
|
||||
external_ids
|
||||
.get(package_id)
|
||||
.map_or(package_id, String::as_str),
|
||||
);
|
||||
}
|
||||
let provider = linked.get(&bundle_pkg.hash).and_then(|info| info.provider);
|
||||
if provider.is_none() && external_ids.values().any(|id| id == package_id) {
|
||||
external_bundle_key(package_id)
|
||||
} else {
|
||||
managed_bundle_key(ProviderId::Modrinth, package_id)
|
||||
managed_bundle_key(provider.unwrap_or(ProviderId::Modrinth), package_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue