hexomc_lib/mods/
updater.rs1use std::collections::HashMap;
2use std::path::Path;
3
4use sha1::{Digest, Sha1};
5
6use crate::{
7 download::{download_file, DownloadTask},
8 error::{HexoError, Result},
9 mods::{
10 curseforge::{CfModFile, CurseForgeClient},
11 detector::{detect_mods, ModInfo, ModLoader},
12 modrinth::{ModrinthClient, MrVersion},
13 },
14};
15
16#[derive(Debug, Clone)]
18pub enum UpdateSource {
19 Modrinth(MrVersion),
20 CurseForge { file: CfModFile, project_id: u64 },
21}
22
23#[derive(Debug, Clone)]
24pub struct ModUpdate {
25 pub current: ModInfo,
26 pub source: UpdateSource,
27}
28
29impl ModUpdate {
30 pub fn version_name(&self) -> &str {
31 match &self.source {
32 UpdateSource::Modrinth(v) => &v.version_number,
33 UpdateSource::CurseForge { file, .. } => &file.display_name,
34 }
35 }
36
37 pub fn file_name(&self) -> Option<&str> {
38 match &self.source {
39 UpdateSource::Modrinth(v) => v.file_name(),
40 UpdateSource::CurseForge { file, .. } => Some(&file.file_name),
41 }
42 }
43
44 pub fn download_url(&self) -> Option<&str> {
45 match &self.source {
46 UpdateSource::Modrinth(v) => v.download_url(),
47 UpdateSource::CurseForge { file, .. } => file.download_url.as_deref(),
48 }
49 }
50
51 pub fn sha1(&self) -> Option<&str> {
52 match &self.source {
53 UpdateSource::Modrinth(v) => v.sha1(),
54 UpdateSource::CurseForge { file, .. } => file.sha1(),
55 }
56 }
57}
58
59pub async fn check_updates(
66 mods_dir: &Path,
67 mc_version: &str,
68 loader: &ModLoader,
69 modrinth: &ModrinthClient,
70 curseforge: Option<&CurseForgeClient>,
71 mod_id_map: &HashMap<String, u64>,
72) -> Result<Vec<ModUpdate>> {
73 let mods = detect_mods(mods_dir);
74 let mut updates = Vec::new();
75
76 for mod_info in mods {
77 let sha1 = file_sha1(&mod_info.file_path);
78
79 if let Some(hash) = &sha1 {
80 if let Some(latest) = modrinth
81 .get_latest_by_hash(hash, mc_version, loader)
82 .await?
83 {
84 if latest.sha1() != Some(hash.as_str()) {
86 updates.push(ModUpdate {
87 current: mod_info,
88 source: UpdateSource::Modrinth(latest),
89 });
90 }
91 continue;
92 }
93 }
94
95 let (Some(cf), Some(&cf_id)) = (curseforge, mod_id_map.get(&mod_info.mod_id)) else {
96 continue;
97 };
98
99 if let Some(latest_file) = cf.get_latest_file(cf_id, mc_version, loader).await? {
100 if latest_file.display_name != mod_info.version
101 && latest_file.file_name != mod_info.file_name
102 {
103 updates.push(ModUpdate {
104 current: mod_info,
105 source: UpdateSource::CurseForge {
106 file: latest_file,
107 project_id: cf_id,
108 },
109 });
110 }
111 }
112 }
113
114 Ok(updates)
115}
116
117pub async fn update_mod(update: &ModUpdate, mods_dir: &Path) -> Result<()> {
119 let url = update
120 .download_url()
121 .ok_or_else(|| HexoError::DownloadFailed {
122 url: format!("mod {} has no download URL", update.current.name),
123 })?;
124 let file_name = update
125 .file_name()
126 .ok_or_else(|| HexoError::DownloadFailed {
127 url: format!("mod {} has no file name", update.current.name),
128 })?;
129
130 let new_path = mods_dir.join(file_name);
131
132 let task = match update.sha1() {
133 Some(sha1) if !sha1.is_empty() => {
134 DownloadTask::new(url, &new_path).with_sha1(sha1.to_string())
135 }
136 _ => DownloadTask::new(url, &new_path),
137 };
138
139 download_file(&task).await?;
140
141 if update.current.file_path != new_path && update.current.file_path.exists() {
142 tokio::fs::remove_file(&update.current.file_path).await?;
143 }
144
145 Ok(())
146}
147
148fn file_sha1(path: &Path) -> Option<String> {
149 let data = std::fs::read(path).ok()?;
150 let mut hasher = Sha1::new();
151 hasher.update(&data);
152 Some(hex::encode(hasher.finalize()))
153}