From c4fa4f25b362ac76ddc9e45c80de39f52fa00bab Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 1 Oct 2026 11:58:39 +0100 Subject: [PATCH 1/2] fix: update all bugs --- packages/app-lib/src/install/recovery.rs | 36 ++++++++++++++-- .../content_store/commands/instance_files.rs | 14 ++++++ .../instances/commands/content_mutation.rs | 43 +++++++++++++------ 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index c2b12278f80..12a78236aae 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -29,6 +29,10 @@ struct SharedInstanceUpdateRollback { entries: Vec, #[serde(default)] bindings: Vec, + #[serde(default)] + missing_file_ids: std::collections::HashSet, + #[serde(default)] + copied_file_ids: std::collections::HashSet, } pub(super) async fn prepare_instance_update_backup( @@ -60,7 +64,7 @@ pub(super) async fn prepare_instance_update_backup( state, ) .await?; - let files = content_rows::get_instance_files( + let mut files = content_rows::get_instance_files( &metadata.instance.id, &state.pool, ) @@ -75,9 +79,11 @@ pub(super) async fn prepare_instance_update_backup( &metadata.instance.id, ) .await?; + let mut missing_file_ids = std::collections::HashSet::new(); + let mut copied_file_ids = std::collections::HashSet::new(); for binding in &bindings { let file = files - .iter() + .iter_mut() .find(|file| file.id == binding.file_id) .ok_or_else(|| { crate::state::content_store::input( @@ -86,20 +92,28 @@ pub(super) async fn prepare_instance_update_backup( })?; let file_status = state.content_store .check_instance_file(&metadata.instance, file, binding).await?; + file.missing = file_status == crate::state::content_store::InstanceFileStatus::Missing; + if file.missing { + missing_file_ids.insert(file.id.clone()); + continue; + } let content = state.content_store.file_content(file).await?; if file_status != crate::state::content_store::InstanceFileStatus::Healthy - || !matches!(content, crate::state::content_store::FileContent::Stored { .. }) { return Err(crate::state::content_store::input(format!( "Restore or repair {} before updating this instance; its current content cannot be backed up safely", file.relative_path, ))); } + if !matches!(content, crate::state::content_store::FileContent::Stored { .. }) { + copied_file_ids.insert(file.id.clone()); + } } let skipped = files .iter() .filter(|file| { bindings.iter().any(|binding| binding.file_id == file.id) + && !copied_file_ids.contains(&file.id) }) .map(crate::state::content_store::content_file_path) .collect(); @@ -111,6 +125,8 @@ pub(super) async fn prepare_instance_update_backup( files, entries, bindings, + missing_file_ids, + copied_file_ids, }; let instance_path = state .directories @@ -247,6 +263,18 @@ async fn restore_instance_update( )); } for binding in &snapshot.bindings { + if snapshot.missing_file_ids.contains(&binding.file_id) { + continue; + } + if snapshot.copied_file_ids.contains(&binding.file_id) { + let file = snapshot.files.iter().find(|file| file.id == binding.file_id) + .ok_or_else(|| crate::state::content_store::input("Backup content reference has no file record"))?; + let path = backup_path.join(crate::state::content_store::content_file_path(file)); + if crate::state::content_store::hash_file(&path).await?.sha512 != binding.blob_sha512 { + return Err(crate::state::content_store::input("The instance backup contains changed content")); + } + continue; + } if state .content_store .lookup(Some(&binding.blob_sha512), None) @@ -282,6 +310,8 @@ async fn restore_instance_update( &rollback.instance.instance, &snapshot.files, &snapshot.bindings, + &snapshot.missing_file_ids, + &snapshot.copied_file_ids, ) .await?; diff --git a/packages/app-lib/src/state/content_store/commands/instance_files.rs b/packages/app-lib/src/state/content_store/commands/instance_files.rs index 298c7048656..e24ac058b4a 100644 --- a/packages/app-lib/src/state/content_store/commands/instance_files.rs +++ b/packages/app-lib/src/state/content_store/commands/instance_files.rs @@ -122,6 +122,8 @@ impl ContentStore { instance: &Instance, files: &[InstanceFile], bindings: &[InstanceFileStorage], + missing_file_ids: &std::collections::HashSet, + copied_file_ids: &std::collections::HashSet, ) -> crate::Result<()> { let files: HashMap<_, _> = files.iter().map(|file| (file.id.as_str(), file)).collect(); @@ -131,6 +133,18 @@ impl ContentStore { files.get(binding.file_id.as_str()).ok_or_else(|| { input("Rollback content reference has no file record") })?; + if missing_file_ids.contains(&binding.file_id) { + restored.push(( + &binding.file_id, + &binding.blob_sha512, + binding.storage_kind, + )); + continue; + } + if copied_file_ids.contains(&binding.file_id) { + restored.push((&binding.file_id, &binding.blob_sha512, FileStorageKind::Copy)); + continue; + } let stored_file = self .lookup(Some(&binding.blob_sha512), Some(file.size)) .await? diff --git a/packages/app-lib/src/state/instances/commands/content_mutation.rs b/packages/app-lib/src/state/instances/commands/content_mutation.rs index fda62f4d6d2..7fa3af0716f 100644 --- a/packages/app-lib/src/state/instances/commands/content_mutation.rs +++ b/packages/app-lib/src/state/instances/commands/content_mutation.rs @@ -344,7 +344,7 @@ impl<'a> InstanceContent<'a> { ) -> crate::Result { self.content_scope()?; let canonical_path = canonical_content_path(project_path); - let file = content_rows::get_instance_file_by_relative_path( + let mut file = content_rows::get_instance_file_by_relative_path( &self.instance.id, canonical_path, &self.state.pool, @@ -365,6 +365,7 @@ impl<'a> InstanceContent<'a> { ) .await?; let enabled = desired_enabled.unwrap_or(!file.enabled); + file.missing = false; let file_change = match self .state .content_store @@ -393,13 +394,14 @@ impl<'a> InstanceContent<'a> { .await? } InstanceFileStatus::Missing => { + file.missing = !enabled; self.state .content_store .prepare_file_change( &self.instance, FileChangeRequest { relative_path: canonical_path, - replacement: Some(&stored_file), + replacement: enabled.then_some(&stored_file), enabled, legacy_path: None, previous_content: None, @@ -414,17 +416,31 @@ impl<'a> InstanceContent<'a> { } } } - FileContent::Damaged(binding) if !enabled => { - self.state - .content_store - .prepare_file_move(&self.instance, &file, &binding, false) - .await? - } - FileContent::Damaged(_) => { - return Err(input( - "Content needs repair or re-import before it can be enabled", - )); - } + FileContent::Damaged(binding) => { + match self.state.content_store.check_instance_file(&self.instance, &file, &binding).await? { + InstanceFileStatus::Healthy => self.state.content_store + .prepare_file_move(&self.instance, &file, &binding, enabled).await?, + InstanceFileStatus::Missing if !enabled => { + file.missing = true; + self.state.content_store.prepare_file_change( + &self.instance, + FileChangeRequest { + relative_path: canonical_path, + replacement: None, + enabled, + legacy_path: None, + previous_content: None, + }, + ).await? + } + InstanceFileStatus::Missing => return Err(input( + "Content needs repair or re-import before it can be enabled", + )), + InstanceFileStatus::Conflict => return Err(input( + "Content was changed outside the app; resolve the conflict first", + )), + } + } FileContent::Unmanaged => { let physical_path = content_file_path(&file); let path = self @@ -685,7 +701,6 @@ impl<'a> InstanceContent<'a> { PreparedChange::Toggle { file, enabled } => { let mut updated = file.clone(); updated.enabled = *enabled; - updated.missing = false; updated.modified_at = Utc::now(); let updated = content_rows::upsert_instance_file(&updated, &mut tx) From bb2319cb5dba3562377bbaa7f92db11367c6ae2b Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 1 Oct 2026 12:08:27 +0100 Subject: [PATCH 2/2] fix: lint --- packages/app-lib/src/install/recovery.rs | 48 +++++++----- .../content_store/commands/instance_files.rs | 32 ++++---- .../instances/commands/content_mutation.rs | 78 ++++++++++++------- .../src/layouts/shared/content-tab/layout.vue | 4 + 4 files changed, 102 insertions(+), 60 deletions(-) diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index 12a78236aae..e6c7b7e5979 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -29,10 +29,10 @@ struct SharedInstanceUpdateRollback { entries: Vec, #[serde(default)] bindings: Vec, - #[serde(default)] - missing_file_ids: std::collections::HashSet, - #[serde(default)] - copied_file_ids: std::collections::HashSet, + #[serde(default)] + missing_file_ids: std::collections::HashSet, + #[serde(default)] + copied_file_ids: std::collections::HashSet, } pub(super) async fn prepare_instance_update_backup( @@ -263,18 +263,30 @@ async fn restore_instance_update( )); } for binding in &snapshot.bindings { - if snapshot.missing_file_ids.contains(&binding.file_id) { - continue; - } - if snapshot.copied_file_ids.contains(&binding.file_id) { - let file = snapshot.files.iter().find(|file| file.id == binding.file_id) - .ok_or_else(|| crate::state::content_store::input("Backup content reference has no file record"))?; - let path = backup_path.join(crate::state::content_store::content_file_path(file)); - if crate::state::content_store::hash_file(&path).await?.sha512 != binding.blob_sha512 { - return Err(crate::state::content_store::input("The instance backup contains changed content")); - } - continue; - } + if snapshot.missing_file_ids.contains(&binding.file_id) { + continue; + } + if snapshot.copied_file_ids.contains(&binding.file_id) { + let file = snapshot + .files + .iter() + .find(|file| file.id == binding.file_id) + .ok_or_else(|| { + crate::state::content_store::input( + "Backup content reference has no file record", + ) + })?; + let path = backup_path + .join(crate::state::content_store::content_file_path(file)); + if crate::state::content_store::hash_file(&path).await?.sha512 + != binding.blob_sha512 + { + return Err(crate::state::content_store::input( + "The instance backup contains changed content", + )); + } + continue; + } if state .content_store .lookup(Some(&binding.blob_sha512), None) @@ -310,8 +322,8 @@ async fn restore_instance_update( &rollback.instance.instance, &snapshot.files, &snapshot.bindings, - &snapshot.missing_file_ids, - &snapshot.copied_file_ids, + &snapshot.missing_file_ids, + &snapshot.copied_file_ids, ) .await?; diff --git a/packages/app-lib/src/state/content_store/commands/instance_files.rs b/packages/app-lib/src/state/content_store/commands/instance_files.rs index e24ac058b4a..406afd52919 100644 --- a/packages/app-lib/src/state/content_store/commands/instance_files.rs +++ b/packages/app-lib/src/state/content_store/commands/instance_files.rs @@ -122,8 +122,8 @@ impl ContentStore { instance: &Instance, files: &[InstanceFile], bindings: &[InstanceFileStorage], - missing_file_ids: &std::collections::HashSet, - copied_file_ids: &std::collections::HashSet, + missing_file_ids: &std::collections::HashSet, + copied_file_ids: &std::collections::HashSet, ) -> crate::Result<()> { let files: HashMap<_, _> = files.iter().map(|file| (file.id.as_str(), file)).collect(); @@ -133,18 +133,22 @@ impl ContentStore { files.get(binding.file_id.as_str()).ok_or_else(|| { input("Rollback content reference has no file record") })?; - if missing_file_ids.contains(&binding.file_id) { - restored.push(( - &binding.file_id, - &binding.blob_sha512, - binding.storage_kind, - )); - continue; - } - if copied_file_ids.contains(&binding.file_id) { - restored.push((&binding.file_id, &binding.blob_sha512, FileStorageKind::Copy)); - continue; - } + if missing_file_ids.contains(&binding.file_id) { + restored.push(( + &binding.file_id, + &binding.blob_sha512, + binding.storage_kind, + )); + continue; + } + if copied_file_ids.contains(&binding.file_id) { + restored.push(( + &binding.file_id, + &binding.blob_sha512, + FileStorageKind::Copy, + )); + continue; + } let stored_file = self .lookup(Some(&binding.blob_sha512), Some(file.size)) .await? diff --git a/packages/app-lib/src/state/instances/commands/content_mutation.rs b/packages/app-lib/src/state/instances/commands/content_mutation.rs index 7fa3af0716f..76197116675 100644 --- a/packages/app-lib/src/state/instances/commands/content_mutation.rs +++ b/packages/app-lib/src/state/instances/commands/content_mutation.rs @@ -365,7 +365,7 @@ impl<'a> InstanceContent<'a> { ) .await?; let enabled = desired_enabled.unwrap_or(!file.enabled); - file.missing = false; + file.missing = false; let file_change = match self .state .content_store @@ -394,14 +394,15 @@ impl<'a> InstanceContent<'a> { .await? } InstanceFileStatus::Missing => { - file.missing = !enabled; + file.missing = !enabled; self.state .content_store .prepare_file_change( &self.instance, FileChangeRequest { relative_path: canonical_path, - replacement: enabled.then_some(&stored_file), + replacement: enabled + .then_some(&stored_file), enabled, legacy_path: None, previous_content: None, @@ -416,31 +417,52 @@ impl<'a> InstanceContent<'a> { } } } - FileContent::Damaged(binding) => { - match self.state.content_store.check_instance_file(&self.instance, &file, &binding).await? { - InstanceFileStatus::Healthy => self.state.content_store - .prepare_file_move(&self.instance, &file, &binding, enabled).await?, - InstanceFileStatus::Missing if !enabled => { - file.missing = true; - self.state.content_store.prepare_file_change( - &self.instance, - FileChangeRequest { - relative_path: canonical_path, - replacement: None, - enabled, - legacy_path: None, - previous_content: None, - }, - ).await? - } - InstanceFileStatus::Missing => return Err(input( - "Content needs repair or re-import before it can be enabled", - )), - InstanceFileStatus::Conflict => return Err(input( - "Content was changed outside the app; resolve the conflict first", - )), - } - } + FileContent::Damaged(binding) => { + match self + .state + .content_store + .check_instance_file(&self.instance, &file, &binding) + .await? + { + InstanceFileStatus::Healthy => { + self.state + .content_store + .prepare_file_move( + &self.instance, + &file, + &binding, + enabled, + ) + .await? + } + InstanceFileStatus::Missing if !enabled => { + file.missing = true; + self.state + .content_store + .prepare_file_change( + &self.instance, + FileChangeRequest { + relative_path: canonical_path, + replacement: None, + enabled, + legacy_path: None, + previous_content: None, + }, + ) + .await? + } + InstanceFileStatus::Missing => { + return Err(input( + "Content needs repair or re-import before it can be enabled", + )); + } + InstanceFileStatus::Conflict => { + return Err(input( + "Content was changed outside the app; resolve the conflict first", + )); + } + } + } FileContent::Unmanaged => { let physical_path = content_file_path(&file); let path = self diff --git a/packages/ui/src/layouts/shared/content-tab/layout.vue b/packages/ui/src/layouts/shared/content-tab/layout.vue index 85df3a09628..ea8ac4d8c8c 100644 --- a/packages/ui/src/layouts/shared/content-tab/layout.vue +++ b/packages/ui/src/layouts/shared/content-tab/layout.vue @@ -900,6 +900,10 @@ async function openUpdateAll(items: ContentItem[]) { return const candidates = getUpdateAllCandidates(items) if (candidates.length === 0) return + if (candidates.length === 1 && ctx.updateItem) { + handleUpdateById(getItemId(candidates[0])) + return + } const requestId = ++updateAllRequestId updateAllItems.value = []