fix: bulk update updating linked content (#6517)

* fix: bulk update updating linked content

* fix: disabled content sometimes desyncing

* fix: bad filter

* fix: fmt
This commit is contained in:
Calum H.
2026-06-26 17:50:15 +02:00
committed by GitHub
parent e4376c2b15
commit ebf4a6ef5f
9 changed files with 114 additions and 22 deletions
+1
View File
@@ -97,6 +97,7 @@ type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge' export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = { type ContentFile = {
enabled: boolean
metadata?: { metadata?: {
project_id: string project_id: string
version_id: string version_id: string
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmissing = 0,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380"
}
@@ -117,6 +117,7 @@ impl ModLoader {
pub struct ContentFile { pub struct ContentFile {
pub hash: String, pub hash: String,
pub file_name: String, pub file_name: String,
pub enabled: bool,
pub size: u64, pub size: u64,
pub metadata: Option<FileMetadata>, pub metadata: Option<FileMetadata>,
pub update_version_id: Option<String>, pub update_version_id: Option<String>,
@@ -627,6 +627,7 @@ pub(crate) async fn rename_instance_file(
relative_path = ?, relative_path = ?,
file_name = ?, file_name = ?,
enabled = ?, enabled = ?,
missing = 0,
modified_at = ? modified_at = ?
WHERE instance_id = ? AND relative_path = ? WHERE instance_id = ? AND relative_path = ?
", ",
@@ -856,11 +857,11 @@ pub(crate) async fn set_content_entry_enabled_for_file(
file_id: &str, file_id: &str,
enabled: bool, enabled: bool,
pool: &SqlitePool, pool: &SqlitePool,
) -> crate::Result<()> { ) -> crate::Result<bool> {
let enabled = i64::from(enabled); let enabled = i64::from(enabled);
let modified_at = Utc::now().timestamp(); let modified_at = Utc::now().timestamp();
sqlx::query!( let result = sqlx::query!(
" "
UPDATE instance_content_entries UPDATE instance_content_entries
SET enabled = ?, modified_at = ? SET enabled = ?, modified_at = ?
@@ -874,7 +875,7 @@ pub(crate) async fn set_content_entry_enabled_for_file(
.execute(pool) .execute(pool)
.await?; .await?;
Ok(()) Ok(result.rows_affected() > 0)
} }
pub(crate) async fn remove_content_entries_for_file( pub(crate) async fn remove_content_entries_for_file(
@@ -661,13 +661,31 @@ pub(crate) async fn toggle_disable_project(
} }
None => index_existing_file(&scope, &new_path, state).await?, None => index_existing_file(&scope, &new_path, state).await?,
}; };
content_rows::set_content_entry_enabled_for_file( let updated_entry = content_rows::set_content_entry_enabled_for_file(
&scope.content_set_id, &scope.content_set_id,
&file.id, &file.id,
enabled, enabled,
&state.pool, &state.pool,
) )
.await?; .await?;
if !updated_entry {
let project_type = ProjectType::get_from_parent_folder(&new_path)
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unable to infer project type from {new_path}"
))
})?;
upsert_entry_for_file(
&scope,
&file,
project_type,
None,
None,
ContentSourceKind::Local,
&state.pool,
)
.await?;
}
Ok(new_path) Ok(new_path)
} }
@@ -281,12 +281,31 @@ async fn plan_bulk_update(
instance_id: &str, instance_id: &str,
state: &State, state: &State,
) -> crate::Result<BulkUpdatePlan> { ) -> crate::Result<BulkUpdatePlan> {
let updateable_paths =
bulk_updateable_project_paths(instance_id, state).await?;
if updateable_paths.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
dependency_additions: Vec::new(),
});
}
let updates = check_content_updates( let updates = check_content_updates(
instance_id, instance_id,
Some(CacheBehaviour::MustRevalidate), Some(CacheBehaviour::MustRevalidate),
state, state,
) )
.await?; .await?
.into_iter()
.filter(|update| updateable_paths.contains(&update.relative_path))
.collect::<Vec<_>>();
if updates.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
dependency_additions: Vec::new(),
});
}
let content_set = let content_set =
content_rows::get_applied_content_set(instance_id, &state.pool) content_rows::get_applied_content_set(instance_id, &state.pool)
.await? .await?
@@ -320,6 +339,7 @@ async fn plan_bulk_update(
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let version_ids = installed let version_ids = installed
.iter() .iter()
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| project.version_id.clone()) .filter_map(|project| project.version_id.clone())
.chain( .chain(
updates updates
@@ -343,6 +363,7 @@ async fn plan_bulk_update(
let planned_versions = installed let planned_versions = installed
.iter() .iter()
.filter(|project| project.enabled) .filter(|project| project.enabled)
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| { .filter_map(|project| {
let target_version_id = updates_by_path let target_version_id = updates_by_path
.get(&project.relative_path) .get(&project.relative_path)
@@ -379,6 +400,21 @@ async fn plan_bulk_update(
}) })
} }
async fn bulk_updateable_project_paths(
instance_id: &str,
state: &State,
) -> crate::Result<HashSet<String>> {
let items = super::list_content::list_content(
instance_id,
None,
Some(CacheBehaviour::MustRevalidate),
state,
)
.await?;
Ok(items.into_iter().map(|item| item.file_path).collect())
}
async fn installed_projects( async fn installed_projects(
instance_id: &str, instance_id: &str,
content_set: &ContentSet, content_set: &ContentSet,
@@ -208,6 +208,8 @@ pub(crate) async fn list_content(
) )
.await?; .await?;
let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link); let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link);
let linked_modpack_source_kind = linked_modpack_source_kind(&link);
let mut failed_modpack_identifier_lookup = false;
let modpack_ids = if imported_modpack_scope { let modpack_ids = if imported_modpack_scope {
None None
} else { } else {
@@ -226,6 +228,7 @@ pub(crate) async fn list_content(
"Failed to fetch modpack identifiers: {}", "Failed to fetch modpack identifiers: {}",
err err
); );
failed_modpack_identifier_lookup = true;
None None
} }
}, },
@@ -240,6 +243,12 @@ pub(crate) async fn list_content(
} }
} else if let Some(ids) = modpack_ids.as_ref() { } else if let Some(ids) = modpack_ids.as_ref() {
ContentFilter::ExcludeModpack(ids) ContentFilter::ExcludeModpack(ids)
} else if failed_modpack_identifier_lookup {
ContentFilter::ExcludeSourceKind {
source_kind: linked_modpack_source_kind
.unwrap_or(ContentSourceKind::ModrinthModpack),
exclude_untracked: true,
}
} else { } else {
ContentFilter::All ContentFilter::All
}; };
@@ -735,6 +744,9 @@ async fn content_projects_for_scope(
update_version_id, update_version_id,
hash: file.sha1, hash: file.sha1,
file_name: file.file_name, file_name: file.file_name,
enabled: entry.map_or(file.enabled, |entry| {
entry.enabled && file.enabled
}),
size: file.size, size: file.size,
metadata: file_metadata_from_entry_or_cache(entry, metadata), metadata: file_metadata_from_entry_or_cache(entry, metadata),
project_type, project_type,
@@ -882,7 +894,7 @@ async fn content_files_to_content_items(
file_path: path.clone(), file_path: path.clone(),
id: file.hash.clone(), id: file.hash.clone(),
size: file.size, size: file.size,
enabled: !file.file_name.ends_with(".disabled"), enabled: file.enabled,
project_type: file.project_type, project_type: file.project_type,
project: project.map(|project| ContentItemProject { project: project.map(|project| ContentItemProject {
id: project.id.clone(), id: project.id.clone(),
@@ -1097,6 +1109,20 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> {
} }
} }
fn linked_modpack_source_kind(
link: &InstanceLink,
) -> Option<ContentSourceKind> {
match link {
InstanceLink::ModrinthModpack { .. } => {
Some(ContentSourceKind::ModrinthModpack)
}
InstanceLink::ServerProjectModpack { .. } => {
Some(ContentSourceKind::ServerProject)
}
_ => None,
}
}
fn check_modpack_update( fn check_modpack_update(
installed_version_id: &str, installed_version_id: &str,
installed_version: &Version, installed_version: &Version,
@@ -39,9 +39,18 @@ pub(crate) async fn sync_instance_content_files(
&state.api_semaphore, &state.api_semaphore,
) )
.await?; .await?;
let hashes_by_path = hashes let hashes_by_key = hashes
.into_iter() .into_iter()
.map(|hash| (hash.path.clone(), hash)) .map(|hash| {
(
format!(
"{}-{}",
hash.size,
hash.path.trim_end_matches(".disabled")
),
hash,
)
})
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let existing_files = let existing_files =
sqlite::content_rows::get_instance_files(&instance.id, &state.pool) sqlite::content_rows::get_instance_files(&instance.id, &state.pool)
@@ -55,8 +64,8 @@ pub(crate) async fn sync_instance_content_files(
let mut files = Vec::new(); let mut files = Vec::new();
for file in scanned { for file in scanned {
let cache_path = format!("{}/{}", instance.path, file.relative_path); let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
let Some(hash) = hashes_by_path.get(&cache_path) else { let Some(hash) = hashes_by_key.get(hash_key) else {
continue; continue;
}; };
let existing_file = existing_files_by_path.get(&file.relative_path); let existing_file = existing_files_by_path.get(&file.relative_path);