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