1use anyhow::{anyhow, Result};
31use itertools::Itertools;
32use self_update::{backends::github::ReleaseList, Download, get_target, cargo_crate_version, Move, update::Release};
33use tempfile::Builder;
34use zip::ZipArchive;
35
36use std::env::current_exe;
37use std::fmt::Display;
38use std::fs::{self, DirBuilder, File};
39use std::io;
40use std::path::{Path, PathBuf};
41
42use rpfm_ipc::api::ApiError;
43use rpfm_ipc::api::updates::{UpdateComponent, UpdateState, UpdateStatus};
44use rpfm_ipc::helpers::*;
45
46use rpfm_lib::games::{LUA_BRANCH, LUA_REMOTE, LUA_REPO, OLD_AK_BRANCH, OLD_AK_REMOTE, OLD_AK_REPO, TRANSLATIONS_BRANCH, TRANSLATIONS_REMOTE, TRANSLATIONS_REPO};
47use rpfm_lib::integrations::git::{GitIntegration, GitResponse};
48use rpfm_lib::schema::{SCHEMA_BRANCH, SCHEMA_REMOTE, SCHEMA_REPO};
49use rpfm_lib::utils::files_from_subdir;
50
51use rpfm_ipc::settings::{self, lua_autogen_base_path, old_ak_files_path, schemas_path, translations_remote_path, Settings};
52
53const UPDATE_EXTENSION: &str = "zip";
54const REPO_OWNER: &str = "Frodo45127";
55const REPO_NAME: &str = "rpfm";
56
57const UPDATE_FOLDER_PREFIX: &str = "updates";
58
59pub const CHANGELOG_FILE: &str = "Changelog.txt";
62
63pub const STABLE: &str = "Stable";
65
66pub const BETA: &str = "Beta";
68
69pub struct Updater {}
78
79#[derive(Debug, PartialEq, Eq, Copy, Clone)]
88pub enum UpdateChannel {
89 Stable,
91 Beta
93}
94
95pub fn update_main_program(settings: &Settings) -> Result<()> {
105 let channel = update_channel(settings);
106 update_main_program_with(|| last_release(channel))
107}
108
109pub fn update_main_program_with(fetch_release: impl FnOnce() -> Result<Release>) -> Result<()> {
113 let last_release = fetch_release()?;
114
115 let asset = last_release.asset_for(get_target(), None).ok_or_else(|| anyhow!("No download available for your architecture."))?;
117 let mut tmp_path = std::env::current_exe().unwrap();
118 tmp_path.pop();
119 let tmp_dir = Builder::new()
120 .prefix(UPDATE_FOLDER_PREFIX)
121 .tempdir_in(tmp_path)?;
122
123 DirBuilder::new().recursive(true).create(&tmp_dir)?;
124
125 {
127 let tmp_zip_path = tmp_dir.path().join(&asset.name);
128 let tmp_zip = File::create(&tmp_zip_path)?;
129
130 Download::from_url(&asset.download_url)
131 .set_header(reqwest::header::ACCEPT, "application/octet-stream".parse().unwrap())
132 .download_to(&tmp_zip)?;
133
134 extract_zip(&tmp_zip_path, tmp_dir.path()).map_err(|e| anyhow!("There was an error while extracting the update. This means either I uploaded a broken file, or your download was incomplete. In any case, no changes have been done so… try again later: {e}"))?;
136 }
137
138 let mut dest_base_path = current_exe()?;
139 dest_base_path.pop();
140
141 for updated_file in &files_from_subdir(tmp_dir.path(), true)? {
142
143 if let Some(extension) = updated_file.extension() {
145 if let Some(extension) = extension.to_str() {
146 if extension == UPDATE_EXTENSION {
147 continue;
148 }
149 }
150 }
151
152 let mut tmp_file = updated_file.to_path_buf();
153 tmp_file.set_file_name(format!("{}_replacement_tmp", updated_file.file_name().unwrap().to_str().unwrap()));
154
155 let tmp_file_relative = updated_file.strip_prefix(tmp_dir.path()).unwrap();
157 let dest_file = dest_base_path.join(tmp_file_relative);
158
159 let mut dest_folder = dest_base_path.join(tmp_file_relative);
161 dest_folder.pop();
162 DirBuilder::new().recursive(true).create(&dest_folder)?;
163
164 Move::from_source(updated_file)
165 .replace_using_temp(&tmp_file)
166 .to_dest(&dest_file)?;
167 }
168
169 let changelog_path = dest_base_path.join(CHANGELOG_FILE);
171 let _ = open::that(changelog_path);
172
173 Ok(())
174}
175
176fn extract_zip(source: &Path, into_dir: &Path) -> Result<()> {
183 let file = File::open(source)?;
184 let mut archive = ZipArchive::new(file)?;
185 for i in 0..archive.len() {
186 let mut entry = archive.by_index(i)?;
187 let Some(rel_path) = entry.enclosed_name() else { continue };
188 let dest = into_dir.join(rel_path);
189
190 if entry.is_dir() {
191 fs::create_dir_all(&dest)?;
192 } else {
193 if let Some(parent) = dest.parent() {
194 fs::create_dir_all(parent)?;
195 }
196 let mut out = File::create(&dest)?;
197 io::copy(&mut entry, &mut out)?;
198 }
199 }
200 Ok(())
201}
202
203pub fn check_updates_rpfm(settings: &Settings) -> Result<APIResponse> {
208 let channel = update_channel(settings);
209 let current_version = cargo_crate_version!();
210 check_updates_rpfm_with(current_version, channel, || last_release(channel))
211}
212
213pub fn check_updates_rpfm_with(current_version_str: &str, update_channel: UpdateChannel, fetch_release: impl FnOnce() -> Result<Release>) -> Result<APIResponse> {
218 let last_release = fetch_release()?;
219
220 let current_version = current_version_str.split('.').map(|x| x.parse::<i32>().unwrap_or(0)).collect::<Vec<i32>>();
221 let last_version = &last_release.version.split('.').map(|x| x.parse::<i32>().unwrap_or(0)).collect::<Vec<i32>>();
222
223 if let UpdateChannel::Stable = update_channel {
226 if current_version[2] >= 99 {
227 return Ok(APIResponse::NewStableUpdate(format!("v{}", last_version.iter().map(|x| x.to_string()).join("."))));
228 }
229 }
230
231 let first = (last_version[0], current_version[0]);
233 let second = (last_version[1], current_version[1]);
234 let third = (last_version[2], current_version[2]);
235
236 if first.0 == 0 && second.0 == 0 && third.0 == 0 || first.1 == 0 && second.1 == 0 && third.1 == 0 {
238 Ok(APIResponse::UnknownVersion)
239 }
240
241 else if last_version != ¤t_version {
243
244 if first.0 < first.1 { Ok(APIResponse::NoUpdate) }
247
248 else if first.0 > first.1 {
251 match update_channel {
252 UpdateChannel::Stable => Ok(APIResponse::NewStableUpdate(format!("v{}", last_version.iter().map(|x| x.to_string()).join(".")))),
253 UpdateChannel::Beta => Ok(APIResponse::NewBetaUpdate(format!("v{}", last_version.iter().map(|x| x.to_string()).join(".")))),
254 }
255 }
256
257 else if second.0 < second.1 { Ok(APIResponse::NoUpdate) }
260
261 else if second.0 > second.1 {
263 match update_channel {
264 UpdateChannel::Stable => Ok(APIResponse::NewStableUpdate(format!("v{}", last_version.iter().map(|x| x.to_string()).join(".")))),
265 UpdateChannel::Beta => Ok(APIResponse::NewBetaUpdate(format!("v{}", last_version.iter().map(|x| x.to_string()).join(".")))),
266 }
267 }
268
269 else if third.0 < third.1 { Ok(APIResponse::NoUpdate) }
272
273 else if third.0 > third.1 {
275 match update_channel {
276 UpdateChannel::Stable => Ok(APIResponse::NewUpdateHotfix(format!("v{}", last_version.iter().map(|x| x.to_string()).join(".")))),
277 UpdateChannel::Beta => Ok(APIResponse::NewBetaUpdate(format!("v{}", last_version.iter().map(|x| x.to_string()).join(".")))),
278 }
279 }
280
281 else { unreachable!("check_updates") }
283 }
284 else {
285 Ok(APIResponse::NoUpdate)
286 }
287}
288
289pub fn last_release(update_channel: UpdateChannel) -> Result<Release> {
294 let releases = ReleaseList::configure()
295 .repo_owner(REPO_OWNER)
296 .repo_name(REPO_NAME)
297 .build()?
298 .fetch()?;
299
300 match releases.iter().find(|release| {
301 match update_channel {
302 UpdateChannel::Stable => release.version.split('.').collect::<Vec<&str>>()[2].parse::<i32>().unwrap_or(0) < 99,
303 UpdateChannel::Beta => true
304 }
305 }) {
306 Some(last_release) => Ok(last_release.clone()),
307 None => Err(anyhow!("Failed to get last release (should never happen)."))
308 }
309}
310
311pub fn update_channel(settings: &Settings) -> UpdateChannel {
314 match &*settings.string("update_channel") {
315 BETA => UpdateChannel::Beta,
316 _ => UpdateChannel::Stable,
317 }
318}
319
320impl Display for UpdateChannel {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
322 Display::fmt(match &self {
323 UpdateChannel::Stable => STABLE,
324 UpdateChannel::Beta => BETA,
325 }, f)
326 }
327}
328
329pub fn git_check_update(path_fn: fn() -> settings::Result<PathBuf>, repo: &str, branch: &str, remote: &str) -> Result<GitResponse> {
338 Ok(GitIntegration::new(&path_fn()?, repo, branch, remote).check_update()?)
339}
340
341pub fn git_update_repo(path_fn: fn() -> settings::Result<PathBuf>, repo: &str, branch: &str, remote: &str) -> Result<()> {
350 Ok(GitIntegration::new(&path_fn()?, repo, branch, remote).update_repo()?)
351}
352
353pub fn check_component(component: UpdateComponent, settings: &Settings) -> Result<UpdateStatus> {
360 let git_status = |response: GitResponse| {
361 let (available, state) = match response {
362 GitResponse::NewUpdate => (true, UpdateState::NewUpdate),
363 GitResponse::NoUpdate => (false, UpdateState::NoUpdate),
364 GitResponse::NoLocalFiles => (true, UpdateState::NoLocalFiles),
365 GitResponse::Diverged => (true, UpdateState::Diverged),
366 };
367
368 UpdateStatus { available, state, version: None }
369 };
370
371 Ok(match component {
372 UpdateComponent::Program => {
373 let (available, state, version) = match check_updates_rpfm(settings)? {
374 APIResponse::NewBetaUpdate(version) => (true, UpdateState::NewBetaUpdate, Some(version)),
375 APIResponse::NewStableUpdate(version) => (true, UpdateState::NewStableUpdate, Some(version)),
376 APIResponse::NewUpdateHotfix(version) => (true, UpdateState::NewUpdateHotfix, Some(version)),
377 APIResponse::NoUpdate => (false, UpdateState::NoUpdate, None),
378 APIResponse::UnknownVersion => (false, UpdateState::UnknownVersion, None),
379 };
380
381 UpdateStatus { available, state, version }
382 }
383 UpdateComponent::Schemas => git_status(git_check_update(schemas_path, SCHEMA_REPO, SCHEMA_BRANCH, SCHEMA_REMOTE)?),
384 UpdateComponent::LuaAutogen => git_status(git_check_update(lua_autogen_base_path, LUA_REPO, LUA_BRANCH, LUA_REMOTE)?),
385 UpdateComponent::OldAssemblyKit => git_status(git_check_update(old_ak_files_path, OLD_AK_REPO, OLD_AK_BRANCH, OLD_AK_REMOTE)?),
386 UpdateComponent::Translations => git_status(git_check_update(translations_remote_path, TRANSLATIONS_REPO, TRANSLATIONS_BRANCH, TRANSLATIONS_REMOTE)?),
387 })
388}
389
390pub fn apply_component(component: UpdateComponent, settings: &Settings) -> Result<()> {
396 match component {
397 UpdateComponent::Program => update_main_program(settings),
398 UpdateComponent::Schemas => Err(ApiError::InvalidParams("Schemas are updated with schema.update, as they need reloading.".to_owned()).into()),
399 UpdateComponent::LuaAutogen => git_update_repo(lua_autogen_base_path, LUA_REPO, LUA_BRANCH, LUA_REMOTE),
400 UpdateComponent::OldAssemblyKit => git_update_repo(old_ak_files_path, OLD_AK_REPO, OLD_AK_BRANCH, OLD_AK_REMOTE),
401 UpdateComponent::Translations => git_update_repo(translations_remote_path, TRANSLATIONS_REPO, TRANSLATIONS_BRANCH, TRANSLATIONS_REMOTE),
402 }
403}