mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
feat: content management changes (#6104)
* feat: change modpack updating flow * fix: pending install state loss * fix: mods.vue perf problems * chore: todo doc * draft: try preload/fix suspense * fix: lint
This commit is contained in:
@@ -875,6 +875,7 @@ impl CachedEntry {
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
for row in query {
|
||||
let parsed_data = if let Some(data) = row.data.clone() {
|
||||
Some(Self::deserialize_cache_value(type_, data, &row.id)?)
|
||||
@@ -882,7 +883,7 @@ impl CachedEntry {
|
||||
None
|
||||
};
|
||||
|
||||
if row.expires <= Utc::now().timestamp() {
|
||||
if row.expires <= now {
|
||||
if cache_behaviour == CacheBehaviour::MustRevalidate {
|
||||
continue;
|
||||
} else {
|
||||
@@ -890,6 +891,19 @@ impl CachedEntry {
|
||||
}
|
||||
}
|
||||
|
||||
let row_id = row.id.clone();
|
||||
let row_alias = row.alias.clone();
|
||||
let remove_matching_key = |x: &&str| {
|
||||
x != &&*row_id
|
||||
&& !row_alias.as_ref().is_some_and(|y| {
|
||||
if type_.case_sensitive_alias().unwrap_or(true) {
|
||||
x == y
|
||||
} else {
|
||||
y.to_lowercase() == x.to_lowercase()
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(data) = parsed_data {
|
||||
if data.get_type() != type_ {
|
||||
return Err(crate::ErrorKind::OtherError(format!(
|
||||
@@ -901,17 +915,7 @@ impl CachedEntry {
|
||||
.as_error());
|
||||
}
|
||||
|
||||
remaining_keys.retain(|x| {
|
||||
x != &&*row.id
|
||||
&& !row.alias.as_ref().is_some_and(|y| {
|
||||
if type_.case_sensitive_alias().unwrap_or(true)
|
||||
{
|
||||
x == y
|
||||
} else {
|
||||
y.to_lowercase() == x.to_lowercase()
|
||||
}
|
||||
})
|
||||
});
|
||||
remaining_keys.retain(remove_matching_key);
|
||||
|
||||
return_vals.push(Self {
|
||||
id: row.id,
|
||||
@@ -920,6 +924,8 @@ impl CachedEntry {
|
||||
data: Some(data),
|
||||
expires: row.expires,
|
||||
});
|
||||
} else {
|
||||
remaining_keys.retain(remove_matching_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
//! ## Data Flow
|
||||
//!
|
||||
//! 1. Frontend calls `get_content_items(profile_path)`
|
||||
//! 2. Backend fetches all installed files via `Profile::get_projects()`
|
||||
//! 3. If profile is linked to a modpack:
|
||||
//! 2. If profile is linked to a modpack:
|
||||
//! - Fetch modpack file hashes from cache (populated during installation)
|
||||
//! - Fallback: re-download .mrpack if cache miss (cleared/expired)
|
||||
//! - Filter out files that belong to the modpack
|
||||
//! 4. For remaining files, fetch project/version/owner metadata in parallel
|
||||
//! 5. Return sorted `ContentItem` list
|
||||
//! - Filter out files that belong to the modpack before update lookup
|
||||
//! 3. For remaining files, fetch project/version/owner metadata in parallel
|
||||
//! 4. Return sorted `ContentItem` list
|
||||
//!
|
||||
//! ## Caching
|
||||
//!
|
||||
@@ -226,12 +225,8 @@ pub async fn get_linked_modpack_info(
|
||||
};
|
||||
|
||||
// Check for updates
|
||||
let (has_update, update_version_id, update_version) = check_modpack_update(
|
||||
profile,
|
||||
&linked_data.version_id,
|
||||
&version,
|
||||
all_versions,
|
||||
);
|
||||
let (has_update, update_version_id, update_version) =
|
||||
check_modpack_update(&linked_data.version_id, &version, all_versions);
|
||||
|
||||
Ok(Some(LinkedModpackInfo {
|
||||
project,
|
||||
@@ -243,10 +238,9 @@ pub async fn get_linked_modpack_info(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Check if a newer compatible version exists for the linked modpack.
|
||||
/// Check if a newer version exists for the linked modpack.
|
||||
/// Returns (has_update, update_version_id, update_version).
|
||||
fn check_modpack_update(
|
||||
profile: &Profile,
|
||||
installed_version_id: &str,
|
||||
installed_version: &Version,
|
||||
all_versions: Option<Vec<Version>>,
|
||||
@@ -255,44 +249,19 @@ fn check_modpack_update(
|
||||
return (false, None, None);
|
||||
};
|
||||
|
||||
// Get the loader as a string for comparison
|
||||
let loader_str = profile.loader.as_str().to_lowercase();
|
||||
let game_version = &profile.game_version;
|
||||
|
||||
// Filter to compatible versions
|
||||
let mut compatible_versions: Vec<&Version> = versions
|
||||
let mut newer_versions: Vec<&Version> = versions
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
// Must support the profile's game version
|
||||
let supports_game = v.game_versions.contains(game_version);
|
||||
|
||||
// Must support the profile's loader
|
||||
// The v2 API replaces "mrpack" with actual loaders from mrpack_loaders,
|
||||
// but if mrpack_loaders is missing, loaders may be just ["mrpack"].
|
||||
// In that case we can't filter by loader, so accept the version.
|
||||
let real_loaders: Vec<_> = v
|
||||
.loaders
|
||||
.iter()
|
||||
.filter(|l| l.to_lowercase() != "mrpack")
|
||||
.collect();
|
||||
let supports_loader = real_loaders.is_empty()
|
||||
|| real_loaders.iter().any(|l| l.to_lowercase() == loader_str);
|
||||
|
||||
supports_game && supports_loader
|
||||
v.id != installed_version_id
|
||||
&& v.date_published > installed_version.date_published
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by date_published descending (newest first)
|
||||
compatible_versions.sort_by_key(|b| std::cmp::Reverse(b.date_published));
|
||||
newer_versions.sort_by_key(|b| std::cmp::Reverse(b.date_published));
|
||||
|
||||
// Find the newest compatible version
|
||||
if let Some(newest) = compatible_versions.first() {
|
||||
// Check if the newest version is different and newer than installed
|
||||
if newest.id != installed_version_id
|
||||
&& newest.date_published > installed_version.date_published
|
||||
{
|
||||
return (true, Some(newest.id.clone()), Some((*newest).clone()));
|
||||
}
|
||||
if let Some(newest) = newer_versions.first() {
|
||||
return (true, Some(newest.id.clone()), Some((*newest).clone()));
|
||||
}
|
||||
|
||||
(false, None, None)
|
||||
@@ -306,10 +275,6 @@ pub async fn get_content_items(
|
||||
pool: &SqlitePool,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Vec<ContentItem>> {
|
||||
let all_files = profile
|
||||
.get_projects(cache_behaviour, pool, fetch_semaphore)
|
||||
.await?;
|
||||
|
||||
let modpack_ids = if let Some(ref linked_data) = profile.linked_data {
|
||||
if linked_data.version_id.is_empty() {
|
||||
None
|
||||
@@ -350,23 +315,35 @@ pub async fn get_content_items(
|
||||
None
|
||||
};
|
||||
|
||||
let user_files: Vec<(String, ProfileFile)> = all_files
|
||||
.into_iter()
|
||||
.filter(|(_, file)| {
|
||||
modpack_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| !ids.is_modpack_file(file))
|
||||
})
|
||||
.collect();
|
||||
let user_files: Vec<(String, ProfileFile)> = if let Some(ids) = &modpack_ids
|
||||
{
|
||||
let filtered_files = profile
|
||||
.get_projects_excluding_modpack_files(
|
||||
&ids.hashes,
|
||||
&ids.project_ids,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
filtered_files.into_iter().collect()
|
||||
} else {
|
||||
let all_files = profile
|
||||
.get_projects(cache_behaviour, pool, fetch_semaphore)
|
||||
.await?;
|
||||
all_files.into_iter().collect()
|
||||
};
|
||||
|
||||
profile_files_to_content_items(
|
||||
let content_items = profile_files_to_content_items(
|
||||
&profile.path,
|
||||
&user_files,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(content_items)
|
||||
}
|
||||
|
||||
/// Pre-fetched metadata for projects, versions, teams, and organizations.
|
||||
|
||||
@@ -12,7 +12,7 @@ use dashmap::DashMap;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::path::Path;
|
||||
@@ -939,41 +939,144 @@ impl Profile {
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
pool: &SqlitePool,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<DashMap<String, ProfileFile>> {
|
||||
self.get_projects_inner(
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_projects_excluding_modpack_files(
|
||||
&self,
|
||||
excluded_hashes: &HashSet<String>,
|
||||
excluded_project_ids: &HashSet<String>,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
pool: &SqlitePool,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<DashMap<String, ProfileFile>> {
|
||||
self.get_projects_inner(
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
Some(excluded_hashes),
|
||||
Some(excluded_project_ids),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_projects_inner(
|
||||
&self,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
pool: &SqlitePool,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
excluded_hashes: Option<&HashSet<String>>,
|
||||
excluded_project_ids: Option<&HashSet<String>>,
|
||||
) -> crate::Result<DashMap<String, ProfileFile>> {
|
||||
let (keys, file_hashes) =
|
||||
self.scan_and_hash(pool, fetch_semaphore).await?;
|
||||
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.map(|x| Self::get_cache_key(x, self))
|
||||
let excluded_hashes = excluded_hashes.filter(|ids| !ids.is_empty());
|
||||
let excluded_project_ids =
|
||||
excluded_project_ids.filter(|ids| !ids.is_empty());
|
||||
|
||||
let file_hashes = file_hashes
|
||||
.into_iter()
|
||||
.filter(|hash| {
|
||||
excluded_hashes
|
||||
.is_none_or(|excluded| !excluded.contains(&hash.hash))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_hashes_ref =
|
||||
file_hashes.iter().map(|x| &*x.hash).collect::<Vec<_>>();
|
||||
let file_updates_ref =
|
||||
file_updates.iter().map(|x| &**x).collect::<Vec<_>>();
|
||||
let (file_info, file_updates) = tokio::try_join!(
|
||||
CachedEntry::get_file_many(
|
||||
&file_hashes_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
),
|
||||
CachedEntry::get_file_update_many(
|
||||
&file_updates_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
)?;
|
||||
let (file_hashes, file_info_by_hash, file_updates) =
|
||||
if let Some(excluded_project_ids) = excluded_project_ids {
|
||||
let file_hashes_ref =
|
||||
file_hashes.iter().map(|x| &*x.hash).collect::<Vec<_>>();
|
||||
let file_info = CachedEntry::get_file_many(
|
||||
&file_hashes_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut keys_by_path: std::collections::HashMap<
|
||||
String,
|
||||
InitialScanFile,
|
||||
> = keys.into_iter().map(|k| (k.path.clone(), k)).collect();
|
||||
let file_info_by_hash: HashMap<String, CachedFile> = file_info
|
||||
.into_iter()
|
||||
.map(|f| (f.hash.clone(), f))
|
||||
.collect();
|
||||
|
||||
let file_info_by_hash: std::collections::HashMap<String, CachedFile> =
|
||||
file_info.into_iter().map(|f| (f.hash.clone(), f)).collect();
|
||||
let file_hashes = file_hashes
|
||||
.into_iter()
|
||||
.filter(|hash| {
|
||||
file_info_by_hash.get(&hash.hash).is_none_or(|file| {
|
||||
!excluded_project_ids.contains(&file.project_id)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.filter(|x| file_info_by_hash.contains_key(&x.hash))
|
||||
.map(|x| Self::get_cache_key(x, self))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_updates_ref =
|
||||
file_updates.iter().map(|x| &**x).collect::<Vec<_>>();
|
||||
let file_updates = CachedEntry::get_file_update_many(
|
||||
&file_updates_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
(file_hashes, file_info_by_hash, file_updates)
|
||||
} else {
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.map(|x| Self::get_cache_key(x, self))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_hashes_ref =
|
||||
file_hashes.iter().map(|x| &*x.hash).collect::<Vec<_>>();
|
||||
let file_updates_ref =
|
||||
file_updates.iter().map(|x| &**x).collect::<Vec<_>>();
|
||||
let (file_info, file_updates) = tokio::try_join!(
|
||||
CachedEntry::get_file_many(
|
||||
&file_hashes_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
),
|
||||
CachedEntry::get_file_update_many(
|
||||
&file_updates_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
)?;
|
||||
|
||||
let file_info_by_hash: HashMap<String, CachedFile> = file_info
|
||||
.into_iter()
|
||||
.map(|f| (f.hash.clone(), f))
|
||||
.collect();
|
||||
|
||||
(file_hashes, file_info_by_hash, file_updates)
|
||||
};
|
||||
|
||||
let mut keys_by_path: HashMap<String, InitialScanFile> =
|
||||
keys.into_iter().map(|k| (k.path.clone(), k)).collect();
|
||||
|
||||
let mut updates_by_hash: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for update in file_updates {
|
||||
updates_by_hash
|
||||
.entry(update.hash)
|
||||
.or_default()
|
||||
.push(update.update_version_id);
|
||||
}
|
||||
|
||||
let files = DashMap::new();
|
||||
|
||||
@@ -989,11 +1092,8 @@ impl Profile {
|
||||
);
|
||||
|
||||
let update_version_id = if let Some(metadata) = &file {
|
||||
let update_ids: Vec<String> = file_updates
|
||||
.iter()
|
||||
.filter(|x| x.hash == hash.hash)
|
||||
.map(|x| x.update_version_id.clone())
|
||||
.collect();
|
||||
let update_ids =
|
||||
updates_by_hash.remove(&hash.hash).unwrap_or_default();
|
||||
|
||||
if !update_ids.contains(&metadata.version_id) {
|
||||
update_ids.into_iter().next()
|
||||
|
||||
Reference in New Issue
Block a user