From 0176b6986f3dc03c7f81f552e06bea020b29edd4 Mon Sep 17 00:00:00 2001 From: Anten Skrabec Date: Wed, 29 Jul 2026 20:52:19 -0600 Subject: [PATCH] feat(convoy): include Forge mod GUIDs in convoy catalog Store the Forge GUID in the installed_mods table (migration 021) and thread it through all install paths. The convoy catalog now exposes each mod's GUID, falling back to reading package.json on disk when the DB value is NULL (covers manually-installed mods). Co-Authored-By: Claude Opus 4.6 (1M context) --- migrations/021_mod_guid.sql | 1 + src/backup.rs | 7 +++ src/cli/apply.rs | 1 + src/cli/install.rs | 11 ++++ src/cli/list.rs | 1 + src/cli/migrate.rs | 1 + src/cli/remove.rs | 59 +++++++++++++++-- src/cli/setup.rs | 1 + src/cli/update.rs | 1 + src/config_mgmt/mod.rs | 2 +- src/convoy/catalog.rs | 35 +++++++++++ src/convoy/migrate.rs | 2 + src/db/addons.rs | 28 ++++++++- src/db/backups.rs | 1 + src/db/mods.rs | 122 +++++++++++++++++++++++++++--------- src/db/schema.rs | 1 + src/db/tests.rs | 18 ++++++ src/health.rs | 7 +++ src/ops.rs | 70 +++++++++++++++++++-- src/web/handlers/queue.rs | 1 + src/web/install.rs | 2 + src/web/mod_zip_cache.rs | 6 ++ tests/common/mod.rs | 13 +++- 23 files changed, 348 insertions(+), 43 deletions(-) create mode 100644 migrations/021_mod_guid.sql diff --git a/migrations/021_mod_guid.sql b/migrations/021_mod_guid.sql new file mode 100644 index 00000000..e549be8c --- /dev/null +++ b/migrations/021_mod_guid.sql @@ -0,0 +1 @@ +ALTER TABLE installed_mods ADD COLUMN guid TEXT; diff --git a/src/backup.rs b/src/backup.rs index d461c092..537bac26 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -460,6 +460,7 @@ pub fn restore_mod_backup( version, "forge", None, + None, )? } }; @@ -570,6 +571,7 @@ pub fn restore_full_backup( &mm.version, "forge", None, + None, )?; restore_manifest_files( @@ -814,6 +816,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -924,6 +927,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let mut config = crate::config::Config::default(); @@ -1043,6 +1047,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -1096,6 +1101,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -1114,6 +1120,7 @@ mod tests { "2.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( diff --git a/src/cli/apply.rs b/src/cli/apply.rs index 7cb39693..1cf9f23b 100644 --- a/src/cli/apply.rs +++ b/src/cli/apply.rs @@ -271,6 +271,7 @@ pub async fn drain_all(ctx: &CliContext) -> Result { archive_path: archive, source, source_url: op.source_url.as_deref(), + guid: None, }, ) { Ok(db_id) => db_id, diff --git a/src/cli/install.rs b/src/cli/install.rs index 8157424a..081aa8ca 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -517,6 +517,7 @@ async fn install_deps(ctx: &CliContext, deps: &[PendingInstall]) -> Result<()> { name: &dep.name, slug: dep_mod.slug.as_deref(), version: &dep.version, + guid: dep_mod.guid.as_deref(), }, ) .await?; @@ -546,6 +547,7 @@ async fn install_main_mod( name: &forge_mod.name, slug: forge_mod.slug.as_deref(), version: &selected_version.version, + guid: forge_mod.guid.as_deref(), }, ) .await @@ -591,6 +593,7 @@ pub struct ModInstallParams<'a> { pub name: &'a str, pub slug: Option<&'a str>, pub version: &'a str, + pub guid: Option<&'a str>, } /// Download a mod archive from Forge, extract it, and record it in the database. @@ -615,6 +618,7 @@ pub async fn download_and_install( name, slug, version, + guid, } = params; let tmp_dir = tempfile::tempdir().context("failed to create temp directory")?; @@ -640,6 +644,7 @@ pub async fn download_and_install( archive_path: &archive_path, source: crate::ops::ModSource::Forge, source_url: None, + guid: *guid, })?; let file_count = db.get_files_for_mod(db_id)?.len(); @@ -667,6 +672,7 @@ pub async fn download_and_install_with_arc( name, slug, version, + guid, } = params; let tmp_dir = tempfile::tempdir().context("failed to create temp directory")?; @@ -694,6 +700,7 @@ pub async fn download_and_install_with_arc( archive_path: &archive_path, source: crate::ops::ModSource::Forge, source_url: None, + guid: *guid, })? }; @@ -841,6 +848,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -981,6 +989,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -1166,6 +1175,7 @@ async fn install_from_url( archive_path: &archive_path, source: crate::ops::ModSource::Url, source_url: Some(url), + guid: None, })?; println!("\n{mod_name} installed successfully (ID: {db_id})."); @@ -1217,6 +1227,7 @@ async fn install_from_file( archive_path: &archive_path, source: crate::ops::ModSource::File, source_url: None, + guid: None, })?; println!("\n{mod_name} installed successfully (ID: {db_id})."); diff --git a/src/cli/list.rs b/src/cli/list.rs index 6e680839..960ab902 100644 --- a/src/cli/list.rs +++ b/src/cli/list.rs @@ -509,6 +509,7 @@ mod tests { source: "forge".to_string(), source_url: None, group_id: None, + guid: None, } } diff --git a/src/cli/migrate.rs b/src/cli/migrate.rs index b4a9740b..5689ed35 100644 --- a/src/cli/migrate.rs +++ b/src/cli/migrate.rs @@ -557,6 +557,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .expect("insert mod"); db.insert_file( diff --git a/src/cli/remove.rs b/src/cli/remove.rs index dcb58a73..7bdb06ae 100644 --- a/src/cli/remove.rs +++ b/src/cli/remove.rs @@ -274,6 +274,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -294,6 +295,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -314,6 +316,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -351,6 +354,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); ctx.db @@ -379,11 +383,29 @@ mod tests { let mod_c = ctx .db - .insert_mod(Some(100), Some(200), "ModC", None, "1.0.0", "forge", None) + .insert_mod( + Some(100), + Some(200), + "ModC", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); let mod_b = ctx .db - .insert_mod(Some(101), Some(201), "ModB", None, "1.0.0", "forge", None) + .insert_mod( + Some(101), + Some(201), + "ModB", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); // B depends on C ctx.db @@ -409,15 +431,42 @@ mod tests { let mod_c = ctx .db - .insert_mod(Some(100), Some(200), "ModC", None, "1.0.0", "forge", None) + .insert_mod( + Some(100), + Some(200), + "ModC", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); let mod_b = ctx .db - .insert_mod(Some(101), Some(201), "ModB", None, "1.0.0", "forge", None) + .insert_mod( + Some(101), + Some(201), + "ModB", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); let mod_a = ctx .db - .insert_mod(Some(102), Some(202), "ModA", None, "1.0.0", "forge", None) + .insert_mod( + Some(102), + Some(202), + "ModA", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); // A depends on B, B depends on C ctx.db diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 711e1a56..98eea910 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -230,6 +230,7 @@ async fn install_from_forge( name: &forge_mod.name, slug: forge_mod.slug.as_deref(), version: version_str, + guid: forge_mod.guid.as_deref(), }, ) .await?; diff --git a/src/cli/update.rs b/src/cli/update.rs index 6fdc14c0..db7d1e0a 100644 --- a/src/cli/update.rs +++ b/src/cli/update.rs @@ -575,6 +575,7 @@ mod tests { source: "forge".to_string(), source_url: None, group_id: None, + guid: None, }]; assert_eq!(mod_name_for_id(&mods, 100), "TestMod"); diff --git a/src/config_mgmt/mod.rs b/src/config_mgmt/mod.rs index 74b58e36..23dafe34 100644 --- a/src/config_mgmt/mod.rs +++ b/src/config_mgmt/mod.rs @@ -344,7 +344,7 @@ mod tests { let mgr = ConfigManager::new(&QumaDirs::from_legacy(spt_dir.to_path_buf())); let db = crate::db::Database::open_in_memory().unwrap(); // Insert a mod matching the directory name - db.insert_mod(None, None, "TestMod", None, "1.0.0", "manual", None) + db.insert_mod(None, None, "TestMod", None, "1.0.0", "manual", None, None) .unwrap(); let configs = mgr.discover_configs(&db).unwrap(); diff --git a/src/convoy/catalog.rs b/src/convoy/catalog.rs index c5dcf4b9..f043b626 100644 --- a/src/convoy/catalog.rs +++ b/src/convoy/catalog.rs @@ -41,12 +41,39 @@ pub struct CatalogGroup { pub struct CatalogMod { pub id: i64, pub forge_id: Option, + pub guid: Option, pub name: String, pub version: String, pub file_checksums: BTreeMap, pub bundle_checksums: BTreeMap, } +fn resolve_guid( + m: &crate::db::mods::InstalledMod, + spt_dir: &Path, + db: &Database, +) -> Option { + if m.guid.is_some() { + return m.guid.clone(); + } + let files = db.get_files_for_mod_ids(&[m.id]).ok()?; + let mod_dir = files.iter().find_map(|f| { + f.file_path + .strip_prefix("SPT/user/mods/") + .and_then(|rest| rest.split('/').next()) + })?; + let pkg_path = spt_dir + .join("SPT/user/mods") + .join(mod_dir) + .join("package.json"); + let content = std::fs::read_to_string(&pkg_path).ok()?; + let v: serde_json::Value = serde_json::from_str(&content).ok()?; + v.get("name") + .or_else(|| v.get("uid")) + .and_then(|n| n.as_str()) + .map(|s| s.to_string()) +} + fn get_client_file_checksums( db: &Database, m: &crate::db::mods::InstalledMod, @@ -172,9 +199,11 @@ pub fn generate_catalog( let checksums = get_client_file_checksums(db, m)?; if !checksums.is_empty() { let bundle_checksums = get_bundle_checksums(spt_dir, db, m.id); + let guid = resolve_guid(m, spt_dir, db); default_mods.push(CatalogMod { id: m.id, forge_id: m.forge_mod_id, + guid, name: m.name.clone(), version: m.version.clone(), file_checksums: checksums, @@ -203,9 +232,11 @@ pub fn generate_catalog( let checksums = get_client_file_checksums(db, m)?; if !checksums.is_empty() { let bundle_checksums = get_bundle_checksums(spt_dir, db, m.id); + let guid = resolve_guid(m, spt_dir, db); catalog_mods.push(CatalogMod { id: m.id, forge_id: m.forge_mod_id, + guid, name: m.name.clone(), version: m.version.clone(), file_checksums: checksums, @@ -592,6 +623,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -627,6 +659,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.set_mod_group(mod_id, Some(group_id)).unwrap(); @@ -658,6 +691,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -686,6 +720,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); // Only has SPT/user/mods/ files, no BepInEx/ files diff --git a/src/convoy/migrate.rs b/src/convoy/migrate.rs index 010042f3..d991823d 100644 --- a/src/convoy/migrate.rs +++ b/src/convoy/migrate.rs @@ -178,6 +178,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let mod_one_id = db.get_mod_by_forge_id(101).unwrap().unwrap().id; @@ -190,6 +191,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let mod_two_id = db.get_mod_by_forge_id(102).unwrap().unwrap().id; diff --git a/src/db/addons.rs b/src/db/addons.rs index 3e9f16c5..44cd7894 100644 --- a/src/db/addons.rs +++ b/src/db/addons.rs @@ -224,6 +224,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let addon_id = db @@ -258,6 +259,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_addon(500, mod_id, 600, "TestAddon", None, "1.0.0", None) @@ -272,10 +274,28 @@ mod tests { fn list_addons_for_mod_returns_only_children() { let db = Database::open_in_memory().unwrap(); let mod1 = db - .insert_mod(Some(100), Some(200), "Mod1", None, "1.0.0", "forge", None) + .insert_mod( + Some(100), + Some(200), + "Mod1", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); let mod2 = db - .insert_mod(Some(101), Some(201), "Mod2", None, "1.0.0", "forge", None) + .insert_mod( + Some(101), + Some(201), + "Mod2", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); db.insert_addon(500, mod1, 600, "Addon1", None, "1.0.0", None) .unwrap(); @@ -301,6 +321,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let addon_id = db @@ -341,6 +362,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let addon_id = db @@ -367,6 +389,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let addon_id = db @@ -392,6 +415,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); let addon_id = db diff --git a/src/db/backups.rs b/src/db/backups.rs index 8ee4cbe2..a2f29fdd 100644 --- a/src/db/backups.rs +++ b/src/db/backups.rs @@ -405,6 +405,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_backup( diff --git a/src/db/mods.rs b/src/db/mods.rs index 4793af44..de71d316 100644 --- a/src/db/mods.rs +++ b/src/db/mods.rs @@ -17,6 +17,7 @@ pub struct InstalledMod { pub source: String, pub source_url: Option, pub group_id: Option, + pub guid: Option, } #[derive(Debug, Clone)] @@ -93,11 +94,12 @@ impl Database { version: &str, source: &str, source_url: Option<&str>, + guid: Option<&str>, ) -> rusqlite::Result { self.conn.execute( - "INSERT INTO installed_mods (forge_mod_id, forge_version_id, name, slug, version, source, source_url) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![forge_mod_id, forge_version_id, name, slug, version, source, source_url], + "INSERT INTO installed_mods (forge_mod_id, forge_version_id, name, slug, version, source, source_url, guid) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![forge_mod_id, forge_version_id, name, slug, version, source, source_url, guid], )?; Ok(self.conn.last_insert_rowid()) } @@ -105,7 +107,7 @@ impl Database { pub fn get_mod(&self, id: i64) -> rusqlite::Result> { self.conn .query_row( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods WHERE id = ?1", params![id], row_to_installed_mod, @@ -116,7 +118,7 @@ impl Database { pub fn get_mod_by_forge_id(&self, forge_mod_id: i64) -> rusqlite::Result> { self.conn .query_row( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods WHERE forge_mod_id = ?1", params![forge_mod_id], row_to_installed_mod, @@ -129,7 +131,7 @@ impl Database { let by_name = self .conn .query_row( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods WHERE LOWER(name) = LOWER(?1)", params![query], row_to_installed_mod, @@ -140,7 +142,7 @@ impl Database { } self.conn .query_row( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods WHERE LOWER(slug) = LOWER(?1)", params![query], row_to_installed_mod, @@ -150,7 +152,7 @@ impl Database { pub fn list_mods(&self) -> rusqlite::Result> { let mut stmt = self.conn.prepare( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods ORDER BY name", )?; let rows = stmt.query_map([], row_to_installed_mod)?; @@ -160,7 +162,7 @@ impl Database { pub fn list_mods_with_file_counts(&self) -> rusqlite::Result> { let mut stmt = self.conn.prepare( "SELECT m.id, m.forge_mod_id, m.forge_version_id, m.name, m.slug, m.version, - m.installed_at, m.updated_at, m.disabled, m.source, m.source_url, m.group_id, + m.installed_at, m.updated_at, m.disabled, m.source, m.source_url, m.group_id, m.guid, COUNT(f.id) as file_count, COALESCE(SUM(f.file_size), 0) as total_size FROM installed_mods m @@ -170,8 +172,8 @@ impl Database { )?; let rows = stmt.query_map([], |row| { let m = row_to_installed_mod(row)?; - let count: i64 = row.get(12)?; - let size: i64 = row.get(13)?; + let count: i64 = row.get(13)?; + let size: i64 = row.get(14)?; Ok((m, count as usize, size)) })?; rows.collect() @@ -183,7 +185,7 @@ impl Database { ) -> rusqlite::Result> { let mut sql = String::from( "SELECT m.id, m.forge_mod_id, m.forge_version_id, m.name, m.slug, m.version, - m.installed_at, m.updated_at, m.disabled, m.source, m.source_url, m.group_id, + m.installed_at, m.updated_at, m.disabled, m.source, m.source_url, m.group_id, m.guid, COUNT(f.id) as file_count, COALESCE(SUM(f.file_size), 0) as total_size FROM installed_mods m @@ -236,8 +238,8 @@ impl Database { param_values.iter().map(|p| p.as_ref()).collect(); let rows = stmt.query_map(params_ref.as_slice(), |row| { let m = row_to_installed_mod(row)?; - let count: i64 = row.get(12)?; - let size: i64 = row.get(13)?; + let count: i64 = row.get(13)?; + let size: i64 = row.get(14)?; Ok((m, count as usize, size)) })?; rows.collect() @@ -685,7 +687,7 @@ impl Database { #[allow(dead_code)] pub fn get_mods_in_group(&self, group_id: i64) -> rusqlite::Result> { let mut stmt = self.conn.prepare( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods WHERE group_id = ?1 ORDER BY name", )?; let rows = stmt.query_map(params![group_id], row_to_installed_mod)?; @@ -695,7 +697,7 @@ impl Database { #[allow(dead_code)] pub fn get_ungrouped_mods(&self) -> rusqlite::Result> { let mut stmt = self.conn.prepare( - "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id + "SELECT id, forge_mod_id, forge_version_id, name, slug, version, installed_at, updated_at, disabled, source, source_url, group_id, guid FROM installed_mods WHERE group_id IS NULL ORDER BY name", )?; let rows = stmt.query_map([], row_to_installed_mod)?; @@ -764,6 +766,7 @@ fn row_to_installed_mod(row: &rusqlite::Row<'_>) -> rusqlite::Result rusqlite::Result<()> { diff --git a/src/db/tests.rs b/src/db/tests.rs index efccdc83..e92ec950 100644 --- a/src/db/tests.rs +++ b/src/db/tests.rs @@ -41,6 +41,7 @@ fn insert_and_get_mod() { "1.0.0", "forge", None, + None, ) .unwrap(); assert!(id > 0); @@ -82,6 +83,7 @@ fn insert_mod_with_no_slug() { "1.0.0", "forge", None, + None, ) .unwrap(); let m = db.get_mod(id).unwrap().expect("mod should exist"); @@ -99,6 +101,7 @@ fn duplicate_forge_mod_id_rejected() { "1.0.0", "forge", None, + None, ) .unwrap(); let result = db.insert_mod( @@ -109,6 +112,7 @@ fn duplicate_forge_mod_id_rejected() { "2.0.0", "forge", None, + None, ); assert!(result.is_err(), "duplicate forge_mod_id should be rejected"); } @@ -125,6 +129,7 @@ fn delete_mod_cascades_to_files_and_deps() { "1.0.0", "forge", None, + None, ) .unwrap(); let mod_b = db @@ -136,6 +141,7 @@ fn delete_mod_cascades_to_files_and_deps() { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -174,6 +180,7 @@ fn insert_and_get_files() { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -211,6 +218,7 @@ fn insert_file_with_no_hash() { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file(mod_id, "plugins/a.dll", None, None).unwrap(); @@ -232,6 +240,7 @@ fn file_path_unique_constraint() { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -253,6 +262,7 @@ fn insert_and_query_dependency() { "1.0.0", "forge", None, + None, ) .unwrap(); let mod_b = db @@ -264,6 +274,7 @@ fn insert_and_query_dependency() { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -300,6 +311,7 @@ fn reverse_dependencies() { "1.0.0", "forge", None, + None, ) .unwrap(); let mod_b = db @@ -311,6 +323,7 @@ fn reverse_dependencies() { "1.0.0", "forge", None, + None, ) .unwrap(); let mod_c = db @@ -322,6 +335,7 @@ fn reverse_dependencies() { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -350,6 +364,7 @@ fn get_all_dependencies_returns_all_edges() { "1.0", "forge", None, + None, ) .unwrap(); let mod_b = db @@ -361,6 +376,7 @@ fn get_all_dependencies_returns_all_edges() { "1.0", "forge", None, + None, ) .unwrap(); let mod_c = db @@ -372,6 +388,7 @@ fn get_all_dependencies_returns_all_edges() { "1.0", "forge", None, + None, ) .unwrap(); @@ -634,6 +651,7 @@ fn lookup_mod_by_name_or_slug() { "3.0.0", "forge", None, + None, ) .unwrap(); diff --git a/src/health.rs b/src/health.rs index beb3e902..e3f9ebb1 100644 --- a/src/health.rs +++ b/src/health.rs @@ -428,6 +428,7 @@ mod tests { source: "forge".to_string(), source_url: None, group_id: None, + guid: None, } } @@ -748,6 +749,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); ctx.db @@ -793,6 +795,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); ctx.db @@ -1097,6 +1100,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -1157,6 +1161,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -1193,6 +1198,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); ctx.db @@ -1238,6 +1244,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); ctx.db diff --git a/src/ops.rs b/src/ops.rs index dffaa125..f385008e 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -224,6 +224,7 @@ pub struct InstallRequest<'a> { pub archive_path: &'a Path, pub source: ModSource, pub source_url: Option<&'a str>, + pub guid: Option<&'a str>, } /// Parameters for installing an addon from a downloaded archive. @@ -268,6 +269,7 @@ pub fn install_mod_from_archive(req: &InstallRequest<'_>) -> Result { req.version, req.source.as_str(), req.source_url, + req.guid, )?; record_extracted_files(req.db, db_id, &extracted)?; tx.commit()?; @@ -1715,6 +1717,7 @@ pub async fn resolve_and_install_deps( name: &dep.name, slug: dep.slug.as_deref(), version: &dep.version, + guid: dep.guid.as_deref(), }, ) .await?; @@ -1780,6 +1783,7 @@ struct PendingDep { name: String, version: String, slug: Option, + guid: Option, } // TODO(debt): no cycle guard — if the Forge API ever returns circular deps, this @@ -1827,6 +1831,7 @@ fn collect_web_deps( name: node.name.clone(), version, slug: node.slug.clone(), + guid: node.guid.clone(), }); } Ok(()) @@ -1870,6 +1875,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -1910,6 +1916,7 @@ mod tests { archive_path: zip1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -1927,6 +1934,7 @@ mod tests { archive_path: zip2.path(), source: ModSource::Forge, source_url: None, + guid: None, }); assert!(result.is_err(), "duplicate forge_mod_id should fail"); @@ -1966,6 +1974,7 @@ mod tests { archive_path: zip_v1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2020,6 +2029,7 @@ mod tests { archive_path: zip_v1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2103,6 +2113,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2161,7 +2172,16 @@ mod tests { // Mod A owns BepInEx/plugins/SharedDir/a.dll let mod_a = db - .insert_mod(Some(100), Some(200), "ModA", None, "1.0.0", "forge", None) + .insert_mod( + Some(100), + Some(200), + "ModA", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); db.insert_file( mod_a, @@ -2173,7 +2193,16 @@ mod tests { // Mod B also has files under BepInEx/plugins/SharedDir/ let mod_b = db - .insert_mod(Some(101), Some(201), "ModB", None, "1.0.0", "forge", None) + .insert_mod( + Some(101), + Some(201), + "ModB", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); db.insert_file( mod_b, @@ -2194,7 +2223,16 @@ mod tests { let db = Database::open_in_memory().unwrap(); let mod_a = db - .insert_mod(Some(100), Some(200), "ModA", None, "1.0.0", "forge", None) + .insert_mod( + Some(100), + Some(200), + "ModA", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); db.insert_file( mod_a, @@ -2205,7 +2243,16 @@ mod tests { .unwrap(); let mod_b = db - .insert_mod(Some(101), Some(201), "ModB", None, "1.0.0", "forge", None) + .insert_mod( + Some(101), + Some(201), + "ModB", + None, + "1.0.0", + "forge", + None, + None, + ) .unwrap(); db.insert_file( mod_b, @@ -2243,6 +2290,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2309,6 +2357,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2354,6 +2403,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2383,6 +2433,7 @@ mod tests { archive_path: zip_a.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2400,6 +2451,7 @@ mod tests { archive_path: zip_b.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2445,6 +2497,7 @@ mod tests { archive_path: zip_v1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2512,6 +2565,7 @@ mod tests { archive_path: zip_v1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2580,6 +2634,7 @@ mod tests { archive_path: zip_v1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2652,6 +2707,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); @@ -2686,6 +2742,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2722,6 +2779,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -2775,6 +2833,7 @@ mod tests { archive_path: zip.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2818,6 +2877,7 @@ mod tests { archive_path: zip_v1.path(), source: ModSource::Forge, source_url: None, + guid: None, }) .unwrap(); @@ -2872,6 +2932,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -2920,6 +2981,7 @@ mod tests { archive_path: zip.path(), source: ModSource::File, source_url: None, + guid: None, }) .unwrap(); diff --git a/src/web/handlers/queue.rs b/src/web/handlers/queue.rs index 28b30a58..aee24aab 100644 --- a/src/web/handlers/queue.rs +++ b/src/web/handlers/queue.rs @@ -435,6 +435,7 @@ pub(super) async fn apply_install(op: &PendingOperation, state: &AppState) -> an archive_path: &archive_owned, source, source_url: source_url.as_deref(), + guid: None, }) }) .await??; diff --git a/src/web/install.rs b/src/web/install.rs index 44bf5b93..0227da34 100644 --- a/src/web/install.rs +++ b/src/web/install.rs @@ -47,6 +47,7 @@ pub async fn web_download_extract_and_record( archive_path: &archive_path, source: crate::ops::ModSource::Forge, source_url: None, + guid: None, }) }) .await??; @@ -85,6 +86,7 @@ pub async fn web_install_from_url( archive_path: &archive_path, source: crate::ops::ModSource::Url, source_url: Some(&url_owned), + guid: None, }) }) .await??; diff --git a/src/web/mod_zip_cache.rs b/src/web/mod_zip_cache.rs index 989e2606..26b1f22c 100644 --- a/src/web/mod_zip_cache.rs +++ b/src/web/mod_zip_cache.rs @@ -371,6 +371,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file( @@ -410,6 +411,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file(1, "SPT/user/mods/a/package.json", Some("h1"), Some(10)) @@ -438,6 +440,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file(2, "SPT/user/mods/b/package.json", Some("h2"), Some(10)) @@ -606,6 +609,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file(1, "BepInEx/plugins/mod/mod.dll", Some("aaa"), Some(11)) @@ -650,6 +654,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file(1, "BepInEx/plugins/mod/mod.dll", Some("aaa"), Some(7)) @@ -688,6 +693,7 @@ mod tests { "1.0.0", "forge", None, + None, ) .unwrap(); db.insert_file(1, "SPT/user/mods/mod/package.json", Some("aaa"), Some(2)) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ba05f924..604d3acf 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -113,8 +113,17 @@ impl TestAppBuilder { // Seed mods using the DB API for (forge_id, name, version) in &self.mods { - db.insert_mod(Some(*forge_id), Some(1), name, None, version, "forge", None) - .expect("failed to insert mod"); + db.insert_mod( + Some(*forge_id), + Some(1), + name, + None, + version, + "forge", + None, + None, + ) + .expect("failed to insert mod"); } // Seed invites