Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) 304ded1984 fix: fmt 2026-06-26 15:27:09 +01:00
Calum H. (IMB11) 51ad88c9d1 fix: bad filter 2026-06-26 15:23:55 +01:00
Calum H. (IMB11) 19caa610b1 fix: disabled content sometimes desyncing 2026-06-26 15:20:03 +01:00
Calum H. (IMB11) 702429c950 fix: bulk update updating linked content 2026-06-26 15:09:45 +01:00
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'
type ContentFile = {
enabled: boolean
metadata?: {
project_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 hash: String,
pub file_name: String,
pub enabled: bool,
pub size: u64,
pub metadata: Option<FileMetadata>,
pub update_version_id: Option<String>,
@@ -627,6 +627,7 @@ pub(crate) async fn rename_instance_file(
relative_path = ?,
file_name = ?,
enabled = ?,
missing = 0,
modified_at = ?
WHERE instance_id = ? AND relative_path = ?
",
@@ -856,11 +857,11 @@ pub(crate) async fn set_content_entry_enabled_for_file(
file_id: &str,
enabled: bool,
pool: &SqlitePool,
) -> crate::Result<()> {
) -> crate::Result<bool> {
let enabled = i64::from(enabled);
let modified_at = Utc::now().timestamp();
sqlx::query!(
let result = sqlx::query!(
"
UPDATE instance_content_entries
SET enabled = ?, modified_at = ?
@@ -874,7 +875,7 @@ pub(crate) async fn set_content_entry_enabled_for_file(
.execute(pool)
.await?;
Ok(())
Ok(result.rows_affected() > 0)
}
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?,
};
content_rows::set_content_entry_enabled_for_file(
let updated_entry = content_rows::set_content_entry_enabled_for_file(
&scope.content_set_id,
&file.id,
enabled,
&state.pool,
)
.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)
}
@@ -281,12 +281,31 @@ async fn plan_bulk_update(
instance_id: &str,
state: &State,
) -> 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(
instance_id,
Some(CacheBehaviour::MustRevalidate),
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 =
content_rows::get_applied_content_set(instance_id, &state.pool)
.await?
@@ -320,6 +339,7 @@ async fn plan_bulk_update(
.collect::<HashMap<_, _>>();
let version_ids = installed
.iter()
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| project.version_id.clone())
.chain(
updates
@@ -343,6 +363,7 @@ async fn plan_bulk_update(
let planned_versions = installed
.iter()
.filter(|project| project.enabled)
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| {
let target_version_id = updates_by_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(
instance_id: &str,
content_set: &ContentSet,
@@ -208,6 +208,8 @@ pub(crate) async fn list_content(
)
.await?;
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 {
None
} else {
@@ -226,6 +228,7 @@ pub(crate) async fn list_content(
"Failed to fetch modpack identifiers: {}",
err
);
failed_modpack_identifier_lookup = true;
None
}
},
@@ -240,6 +243,12 @@ pub(crate) async fn list_content(
}
} else if let Some(ids) = modpack_ids.as_ref() {
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 {
ContentFilter::All
};
@@ -735,6 +744,9 @@ async fn content_projects_for_scope(
update_version_id,
hash: file.sha1,
file_name: file.file_name,
enabled: entry.map_or(file.enabled, |entry| {
entry.enabled && file.enabled
}),
size: file.size,
metadata: file_metadata_from_entry_or_cache(entry, metadata),
project_type,
@@ -882,7 +894,7 @@ async fn content_files_to_content_items(
file_path: path.clone(),
id: file.hash.clone(),
size: file.size,
enabled: !file.file_name.ends_with(".disabled"),
enabled: file.enabled,
project_type: file.project_type,
project: project.map(|project| ContentItemProject {
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(
installed_version_id: &str,
installed_version: &Version,
@@ -39,9 +39,18 @@ pub(crate) async fn sync_instance_content_files(
&state.api_semaphore,
)
.await?;
let hashes_by_path = hashes
let hashes_by_key = hashes
.into_iter()
.map(|hash| (hash.path.clone(), hash))
.map(|hash| {
(
format!(
"{}-{}",
hash.size,
hash.path.trim_end_matches(".disabled")
),
hash,
)
})
.collect::<HashMap<_, _>>();
let existing_files =
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();
for file in scanned {
let cache_path = format!("{}/{}", instance.path, file.relative_path);
let Some(hash) = hashes_by_path.get(&cache_path) else {
let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
let Some(hash) = hashes_by_key.get(hash_key) else {
continue;
};
let existing_file = existing_files_by_path.get(&file.relative_path);