1pub mod config;
7pub mod downloader;
8pub mod notification;
9pub mod platform;
10pub mod rollback;
11pub mod scheduler;
12pub mod signature;
13pub mod state;
14
15use anyhow::{anyhow, Context, Result};
16use base64::Engine;
17use self_update::cargo_crate_version;
18use self_update::version::bump_is_greater;
19use std::fmt;
20use std::fs;
21use std::path::{Path, PathBuf};
22use tempfile::NamedTempFile;
23use tracing::{error, info};
24
25#[derive(Debug, Clone)]
27pub enum UpdateStatus {
28 UpToDate(String),
30 Updated {
32 from_version: String,
33 to_version: String,
34 },
35 Available {
37 current_version: String,
38 latest_version: String,
39 },
40 Failed(String),
42}
43
44fn is_newer_version_static(version1: &str, version2: &str) -> Result<bool, anyhow::Error> {
48 use semver::Version;
49
50 let v1 = Version::parse(version1.trim_start_matches('v'))
51 .map_err(|e| anyhow::anyhow!("Invalid version '{}': {}", version1, e))?;
52
53 let v2 = Version::parse(version2.trim_start_matches('v'))
54 .map_err(|e| anyhow::anyhow!("Invalid version '{}': {}", version2, e))?;
55
56 Ok(v1 > v2)
57}
58
59impl fmt::Display for UpdateStatus {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 UpdateStatus::UpToDate(version) => {
63 write!(f, "[OK] Already running latest version: {}", version)
64 }
65 UpdateStatus::Updated {
66 from_version,
67 to_version,
68 } => {
69 write!(f, "Updated: from {} to {}", from_version, to_version)
70 }
71 UpdateStatus::Available {
72 current_version,
73 latest_version,
74 } => {
75 write!(
76 f,
77 "Update available: {} → {}",
78 current_version, latest_version
79 )
80 }
81 UpdateStatus::Failed(error) => {
82 write!(f, "[ERROR] Update failed: {}", error)
83 }
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct UpdaterConfig {
91 pub bin_name: String,
93 pub repo_owner: String,
95 pub repo_name: String,
97 pub current_version: String,
99 pub show_progress: bool,
101}
102
103impl UpdaterConfig {
104 pub fn new(bin_name: impl Into<String>) -> Self {
106 Self {
107 bin_name: bin_name.into(),
108 repo_owner: "terraphim".to_string(),
109 repo_name: "terraphim-ai".to_string(),
110 current_version: cargo_crate_version!().to_string(),
111 show_progress: true,
112 }
113 }
114
115 pub fn with_version(mut self, version: impl Into<String>) -> Self {
117 self.current_version = version.into();
118 self
119 }
120
121 pub fn with_progress(mut self, show: bool) -> Self {
123 self.show_progress = show;
124 self
125 }
126}
127
128pub struct TerraphimUpdater {
130 config: UpdaterConfig,
131}
132
133impl TerraphimUpdater {
134 pub fn new(config: UpdaterConfig) -> Self {
136 Self { config }
137 }
138
139 pub async fn check_update(&self) -> Result<UpdateStatus> {
141 info!(
142 "Checking for updates: {} v{}",
143 self.config.bin_name, self.config.current_version
144 );
145
146 let repo_owner = self.config.repo_owner.clone();
148 let repo_name = self.config.repo_name.clone();
149 let bin_name = self.config.bin_name.clone();
150 let current_version = self.config.current_version.clone();
151 let show_progress = self.config.show_progress;
152
153 let result = tokio::task::spawn_blocking(move || {
155 match self_update::backends::github::Update::configure()
157 .repo_owner(&repo_owner)
158 .repo_name(&repo_name)
159 .bin_name(&bin_name)
160 .current_version(¤t_version)
161 .show_download_progress(show_progress)
162 .build()
163 {
164 Ok(updater) => {
165 match updater.get_latest_release() {
167 Ok(release) => {
168 let latest_version = release.version.clone();
169
170 match is_newer_version_static(&latest_version, ¤t_version) {
172 Ok(true) => {
173 Ok::<UpdateStatus, anyhow::Error>(UpdateStatus::Available {
174 current_version,
175 latest_version,
176 })
177 }
178 Ok(false) => Ok::<UpdateStatus, anyhow::Error>(
179 UpdateStatus::UpToDate(current_version),
180 ),
181 Err(e) => Err(e),
182 }
183 }
184 Err(e) => Ok(UpdateStatus::Failed(format!("Check failed: {}", e))),
185 }
186 }
187 Err(e) => Ok(UpdateStatus::Failed(format!("Configuration error: {}", e))),
188 }
189 })
190 .await;
191
192 match result {
193 Ok(update_result) => {
194 match update_result {
195 Ok(status) => {
196 match &status {
198 UpdateStatus::Available {
199 current_version,
200 latest_version,
201 } => {
202 info!(
203 "Update available: {} -> {}",
204 current_version, latest_version
205 );
206 }
207 UpdateStatus::UpToDate(version) => {
208 info!("Already up to date: {}", version);
209 }
210 UpdateStatus::Updated {
211 from_version,
212 to_version,
213 } => {
214 info!(
215 "Successfully updated from {} to {}",
216 from_version, to_version
217 );
218 }
219 UpdateStatus::Failed(error) => {
220 error!("Update check failed: {}", error);
221 }
222 }
223 Ok(status)
224 }
225 Err(e) => {
226 error!("Blocking task failed: {}", e);
227 Ok(UpdateStatus::Failed(format!("Blocking task error: {}", e)))
228 }
229 }
230 }
231 Err(e) => {
232 error!("Failed to spawn blocking task: {}", e);
233 Ok(UpdateStatus::Failed(format!("Task spawn error: {}", e)))
234 }
235 }
236 }
237
238 pub async fn update(&self) -> Result<UpdateStatus> {
240 info!(
241 "Updating {} from version {}",
242 self.config.bin_name, self.config.current_version
243 );
244
245 let repo_owner = self.config.repo_owner.clone();
247 let repo_name = self.config.repo_name.clone();
248 let bin_name = self.config.bin_name.clone();
249 let current_version = self.config.current_version.clone();
250 let show_progress = self.config.show_progress;
251
252 let key_bytes = base64::engine::general_purpose::STANDARD
254 .decode(signature::get_embedded_public_key())
255 .context("Failed to decode public key")?;
256
257 if key_bytes.len() != 32 {
259 return Err(anyhow!(
260 "Invalid public key length: {} bytes (expected 32)",
261 key_bytes.len()
262 ));
263 }
264 let mut key_array = [0u8; 32];
265 key_array.copy_from_slice(&key_bytes);
266
267 let result = tokio::task::spawn_blocking(move || {
269 let builder_result = self_update::backends::github::Update::configure()
271 .repo_owner(&repo_owner)
272 .repo_name(&repo_name)
273 .bin_name(&bin_name)
274 .current_version(¤t_version)
275 .show_download_progress(show_progress)
276 .verifying_keys(vec![key_array]) .build();
278
279 match builder_result {
280 Ok(updater) => match updater.update() {
281 Ok(status) => match status {
282 self_update::Status::UpToDate(version) => {
283 Ok::<UpdateStatus, anyhow::Error>(UpdateStatus::UpToDate(version))
284 }
285 self_update::Status::Updated(version) => {
286 Ok::<UpdateStatus, anyhow::Error>(UpdateStatus::Updated {
287 from_version: current_version,
288 to_version: version,
289 })
290 }
291 },
292 Err(e) => Ok(UpdateStatus::Failed(format!("Update failed: {}", e))),
293 },
294 Err(e) => Ok(UpdateStatus::Failed(format!("Configuration error: {}", e))),
295 }
296 })
297 .await;
298
299 match result {
300 Ok(update_result) => {
301 match update_result {
302 Ok(status) => {
303 match &status {
305 UpdateStatus::Updated {
306 from_version,
307 to_version,
308 } => {
309 info!(
310 "Successfully updated from {} to {}",
311 from_version, to_version
312 );
313 }
314 UpdateStatus::UpToDate(version) => {
315 info!("Already up to date: {}", version);
316 }
317 UpdateStatus::Available {
318 current_version,
319 latest_version,
320 } => {
321 info!(
322 "Update available: {} -> {}",
323 current_version, latest_version
324 );
325 }
326 UpdateStatus::Failed(error) => {
327 error!("Update failed: {}", error);
328 }
329 }
330 Ok(status)
331 }
332 Err(e) => {
333 error!("Blocking task failed: {}", e);
334 Ok(UpdateStatus::Failed(format!("Blocking task error: {}", e)))
335 }
336 }
337 }
338 Err(e) => {
339 error!("Failed to spawn blocking task: {}", e);
340 Ok(UpdateStatus::Failed(format!("Task spawn error: {}", e)))
341 }
342 }
343 }
344
345 pub async fn update_with_verification(&self) -> Result<UpdateStatus> {
365 info!(
366 "Updating {} from version {} with signature verification",
367 self.config.bin_name, self.config.current_version
368 );
369
370 let repo_owner = self.config.repo_owner.clone();
372 let repo_name = self.config.repo_name.clone();
373 let bin_name = self.config.bin_name.clone();
374 let current_version = self.config.current_version.clone();
375 let show_progress = self.config.show_progress;
376
377 let result = tokio::task::spawn_blocking(move || {
379 Self::update_with_verification_blocking(
380 &repo_owner,
381 &repo_name,
382 &bin_name,
383 ¤t_version,
384 show_progress,
385 )
386 })
387 .await;
388
389 match result {
390 Ok(Ok(update_status)) => {
391 match &update_status {
392 UpdateStatus::Updated {
393 from_version,
394 to_version,
395 } => {
396 info!(
397 "Successfully updated from {} to {} with verified signature",
398 from_version, to_version
399 );
400 }
401 UpdateStatus::UpToDate(version) => {
402 info!("Already up to date: {}", version);
403 }
404 UpdateStatus::Failed(error) => {
405 error!("Update with verification failed: {}", error);
406 }
407 _ => {}
408 }
409 Ok(update_status)
410 }
411 Ok(Err(e)) => {
412 error!("Blocking task returned error: {}", e);
413 Ok(UpdateStatus::Failed(format!("Update error: {}", e)))
414 }
415 Err(e) => {
416 error!("Blocking task failed: {}", e);
417 Ok(UpdateStatus::Failed(format!("Task spawn error: {}", e)))
418 }
419 }
420 }
421
422 fn update_with_verification_blocking(
424 repo_owner: &str,
425 repo_name: &str,
426 bin_name: &str,
427 current_version: &str,
428 show_progress: bool,
429 ) -> Result<UpdateStatus> {
430 info!(
431 "Starting verified update flow for {} v{}",
432 bin_name, current_version
433 );
434
435 let release =
437 match Self::get_latest_release_info(repo_owner, repo_name, bin_name, current_version) {
438 Ok(release) => release,
439 Err(e) => {
440 return Ok(UpdateStatus::Failed(format!(
441 "Failed to get release info: {}",
442 e
443 )));
444 }
445 };
446
447 let latest_version = &release.version;
448
449 let temp_archive = match Self::download_release_archive(
451 repo_owner,
452 repo_name,
453 bin_name,
454 latest_version,
455 show_progress,
456 ) {
457 Ok(archive) => archive,
458 Err(e) => {
459 return Ok(UpdateStatus::Failed(format!(
460 "Failed to download archive: {}",
461 e
462 )));
463 }
464 };
465
466 let archive_path = temp_archive.path().to_path_buf();
467
468 info!("Verifying signature for archive {:?}", archive_path);
470 let verification_result =
471 match crate::signature::verify_archive_signature(&archive_path, None) {
472 Ok(result) => result,
473 Err(e) => return Ok(UpdateStatus::Failed(format!("Verification error: {}", e))),
474 };
475
476 match verification_result {
477 crate::signature::VerificationResult::Valid => {
478 info!("Signature verification passed - proceeding with installation");
479 }
480 crate::signature::VerificationResult::Invalid { reason } => {
481 let error_msg = format!("Signature verification failed: {}", reason);
482 error!("{}", error_msg);
483 return Ok(UpdateStatus::Failed(error_msg));
484 }
485 crate::signature::VerificationResult::MissingSignature => {
486 let error_msg = "No signature found in archive - refusing to install".to_string();
487 error!("{}", error_msg);
488 return Ok(UpdateStatus::Failed(error_msg));
489 }
490 crate::signature::VerificationResult::Error(msg) => {
491 let error_msg = format!("Verification error: {}", msg);
492 error!("{}", error_msg);
493 return Ok(UpdateStatus::Failed(error_msg));
494 }
495 }
496
497 match Self::install_verified_archive(&archive_path, bin_name) {
499 Ok(_) => {
500 info!("Successfully installed verified update");
501 Ok(UpdateStatus::Updated {
502 from_version: current_version.to_string(),
503 to_version: latest_version.clone(),
504 })
505 }
506 Err(e) => Ok(UpdateStatus::Failed(format!("Installation failed: {}", e))),
507 }
508 }
509
510 fn get_latest_release_info(
512 repo_owner: &str,
513 repo_name: &str,
514 bin_name: &str,
515 current_version: &str,
516 ) -> Result<self_update::update::Release> {
517 info!(
518 "Fetching latest release info for {}/{}",
519 repo_owner, repo_name
520 );
521
522 let updater = self_update::backends::github::Update::configure()
523 .repo_owner(repo_owner)
524 .repo_name(repo_name)
525 .bin_name(bin_name)
526 .current_version(current_version)
527 .build()?;
528
529 let release = updater.get_latest_release()?;
530
531 #[allow(clippy::needless_borrow)]
533 if !bump_is_greater(¤t_version, &release.version)? {
534 return Err(anyhow!(
535 "Current version {} is up to date with {}",
536 current_version,
537 release.version
538 ));
539 }
540
541 info!("Latest version: {}", release.version);
542 Ok(release)
543 }
544
545 fn download_release_archive(
547 repo_owner: &str,
548 repo_name: &str,
549 bin_name: &str,
550 version: &str,
551 show_progress: bool,
552 ) -> Result<NamedTempFile> {
553 let target = Self::get_target_triple()?;
555 let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
556
557 let archive_name = format!("{}-{}-{}.{}", bin_name, version, target, extension);
559 let download_url = format!(
560 "https://github.com/{}/{}/releases/download/{}/{}",
561 repo_owner, repo_name, version, archive_name
562 );
563
564 info!("Downloading from: {}", download_url);
565
566 let temp_file = NamedTempFile::new()?;
568 let download_config = crate::downloader::DownloadConfig {
569 show_progress,
570 ..Default::default()
571 };
572
573 crate::downloader::download_with_retry(
574 &download_url,
575 temp_file.path(),
576 Some(download_config),
577 )?;
578
579 info!("Downloaded archive to: {:?}", temp_file.path());
580 Ok(temp_file)
581 }
582
583 fn get_target_triple() -> Result<String> {
585 use std::env::consts::{ARCH, OS};
586
587 let target = format!("{}-{}", ARCH, OS);
588
589 let target = match target.as_str() {
591 "x86_64-linux" => "x86_64-unknown-linux-gnu".to_string(),
592 "aarch64-linux" => "aarch64-unknown-linux-gnu".to_string(),
593 "x86_64-windows" => "x86_64-pc-windows-msvc".to_string(),
594 "x86_64-macos" => "x86_64-apple-darwin".to_string(),
595 "aarch64-macos" => "aarch64-apple-darwin".to_string(),
596 _ => target,
597 };
598
599 Ok(target)
600 }
601
602 fn install_verified_archive(archive_path: &Path, bin_name: &str) -> Result<()> {
604 info!("Installing verified archive {:?}", archive_path);
605
606 let current_exe = std::env::current_exe()?;
608 let install_dir = current_exe
609 .parent()
610 .ok_or_else(|| anyhow!("Cannot determine install directory"))?;
611
612 info!("Installing to directory: {:?}", install_dir);
613
614 if cfg!(windows) {
617 Self::extract_zip(archive_path, install_dir)?;
619 } else {
620 Self::extract_tarball(archive_path, install_dir, bin_name)?;
622 }
623
624 #[cfg(unix)]
626 {
627 use std::os::unix::fs::PermissionsExt;
628 let bin_path = install_dir.join(bin_name);
629 if bin_path.exists() {
630 let mut perms = fs::metadata(&bin_path)?.permissions();
631 perms.set_mode(0o755);
632 fs::set_permissions(&bin_path, perms)?;
633 }
634 }
635
636 Ok(())
637 }
638
639 fn extract_zip(archive_path: &Path, target_dir: &Path) -> Result<()> {
641 use zip::ZipArchive;
642
643 let file = fs::File::open(archive_path)?;
644 let mut archive = ZipArchive::new(file)?;
645
646 for i in 0..archive.len() {
647 let mut file = archive.by_index(i)?;
648 #[allow(clippy::needless_borrows_for_generic_args)]
649 let outpath = target_dir.join(file.mangled_name());
650
651 if file.name().ends_with('/') {
652 fs::create_dir_all(&outpath)?;
653 } else {
654 if let Some(parent) = outpath.parent() {
655 fs::create_dir_all(parent)?;
656 }
657 let mut outfile = fs::File::create(&outpath)?;
658 std::io::copy(&mut file, &mut outfile)?;
659 }
660 }
661
662 Ok(())
663 }
664
665 fn extract_tarball(archive_path: &Path, target_dir: &Path, bin_name: &str) -> Result<()> {
667 use flate2::read::GzDecoder;
668 use tar::Archive;
669
670 let file = fs::File::open(archive_path)?;
671 let decoder = GzDecoder::new(file);
672 let mut archive = Archive::new(decoder);
673
674 for entry in archive.entries()? {
677 let mut entry = entry?;
678 let path = entry.path()?;
679
680 if let Some(file_name) = path.file_name() {
682 if file_name.to_str() == Some(bin_name) {
683 let outpath = target_dir.join(bin_name);
684 let mut outfile = fs::File::create(&outpath)?;
685 std::io::copy(&mut entry, &mut outfile)?;
686 info!("Extracted binary to {:?}", outpath);
687 break;
688 }
689 }
690 }
691
692 Ok(())
693 }
694
695 pub async fn check_and_update(&self) -> Result<UpdateStatus> {
697 match self.check_update().await? {
698 UpdateStatus::Available {
699 current_version,
700 latest_version,
701 } => {
702 info!(
703 "Update available: {} → {}, installing...",
704 current_version, latest_version
705 );
706 self.update_with_verification().await
707 }
708 status => Ok(status),
709 }
710 }
711
712 #[allow(dead_code)]
714 fn is_newer_version(&self, version1: &str, version2: &str) -> Result<bool> {
715 let v1_parts: Vec<u32> = version1
717 .trim_start_matches('v')
718 .split('.')
719 .take(3)
720 .map(|s| s.parse().unwrap_or(0))
721 .collect();
722
723 let v2_parts: Vec<u32> = version2
724 .trim_start_matches('v')
725 .split('.')
726 .take(3)
727 .map(|s| s.parse().unwrap_or(0))
728 .collect();
729
730 let v1 = [
732 v1_parts.first().copied().unwrap_or(0),
733 v1_parts.get(1).copied().unwrap_or(0),
734 v1_parts.get(2).copied().unwrap_or(0),
735 ];
736
737 let v2 = [
738 v2_parts.first().copied().unwrap_or(0),
739 v2_parts.get(1).copied().unwrap_or(0),
740 v2_parts.get(2).copied().unwrap_or(0),
741 ];
742
743 Ok(v1 > v2)
744 }
745}
746
747pub async fn check_for_updates(bin_name: impl Into<String>) -> Result<UpdateStatus> {
749 let config = UpdaterConfig::new(bin_name);
750 let updater = TerraphimUpdater::new(config);
751 updater.check_update().await
752}
753
754pub async fn update_binary(bin_name: impl Into<String>) -> Result<UpdateStatus> {
756 let config = UpdaterConfig::new(bin_name);
757 let updater = TerraphimUpdater::new(config);
758 updater.check_and_update().await
759}
760
761pub async fn update_binary_silent(bin_name: impl Into<String>) -> Result<UpdateStatus> {
763 let config = UpdaterConfig::new(bin_name).with_progress(false);
764 let updater = TerraphimUpdater::new(config);
765 updater.check_and_update().await
766}
767
768pub async fn check_for_updates_auto(bin_name: &str, current_version: &str) -> Result<UpdateStatus> {
792 info!("Checking for updates: {} v{}", bin_name, current_version);
793
794 let bin_name = bin_name.to_string();
795 let current_version = current_version.to_string();
796
797 let result =
798 tokio::task::spawn_blocking(
799 move || match self_update::backends::github::Update::configure()
800 .repo_owner("terraphim")
801 .repo_name("terraphim-ai")
802 .bin_name(&bin_name)
803 .current_version(¤t_version)
804 .build()
805 {
806 Ok(updater) => match updater.get_latest_release() {
807 Ok(release) => {
808 let latest_version = release.version.clone();
809
810 match is_newer_version_static(&latest_version, ¤t_version) {
811 Ok(true) => {
812 Ok::<UpdateStatus, anyhow::Error>(UpdateStatus::Available {
813 current_version,
814 latest_version,
815 })
816 }
817 Ok(false) => Ok::<UpdateStatus, anyhow::Error>(UpdateStatus::UpToDate(
818 current_version,
819 )),
820 Err(e) => Err(e),
821 }
822 }
823 Err(e) => Ok(UpdateStatus::Failed(format!("Check failed: {}", e))),
824 },
825 Err(e) => Ok(UpdateStatus::Failed(format!("Configuration error: {}", e))),
826 },
827 )
828 .await;
829
830 match result {
831 Ok(update_result) => update_result,
832 Err(e) => {
833 error!("Failed to spawn blocking task: {}", e);
834 Ok(UpdateStatus::Failed(format!("Task spawn error: {}", e)))
835 }
836 }
837}
838
839pub async fn check_for_updates_startup(bin_name: &str) -> Result<UpdateStatus> {
863 let current_version = env!("CARGO_PKG_VERSION");
864 check_for_updates_auto(bin_name, current_version).await
865}
866
867pub async fn start_update_scheduler(
897 bin_name: &str,
898 current_version: &str,
899 callback: Box<dyn Fn(UpdateAvailableInfo) + Send + Sync>,
900) -> Result<tokio::task::JoinHandle<()>> {
901 use crate::config::UpdateConfig;
902 use crate::scheduler::{UpdateCheckResult, UpdateScheduler};
903 use std::sync::Arc;
904
905 let config = UpdateConfig::default();
906
907 let bin_name_clone = bin_name.to_string();
908 let current_version_clone = current_version.to_string();
909
910 let check_fn = Arc::new(move || -> anyhow::Result<UpdateCheckResult> {
911 let status = {
912 let bin_name = bin_name_clone.clone();
913 let current_version = current_version_clone.clone();
914
915 tokio::task::block_in_place(|| {
916 let rt = tokio::runtime::Runtime::new()?;
917 rt.block_on(async { check_for_updates_auto(&bin_name, ¤t_version).await })
918 })
919 }?;
920
921 match status {
922 UpdateStatus::Available {
923 current_version,
924 latest_version,
925 } => Ok(UpdateCheckResult::UpdateAvailable {
926 current_version,
927 latest_version,
928 }),
929 UpdateStatus::UpToDate(_) => Ok(UpdateCheckResult::UpToDate),
930 UpdateStatus::Failed(error) => Ok(UpdateCheckResult::Failed { error }),
931 _ => Ok(UpdateCheckResult::UpToDate),
932 }
933 });
934
935 let mut scheduler = UpdateScheduler::new(Arc::new(config), check_fn);
936 let mut receiver = scheduler.create_notification_channel()?;
937
938 scheduler.start().await?;
939
940 let callback = Arc::new(callback);
941
942 let handle = tokio::spawn(async move {
943 while let Some(notification) = receiver.recv().await {
944 match notification {
945 crate::scheduler::UpdateNotification::UpdateAvailable {
946 current_version,
947 latest_version,
948 } => {
949 callback(UpdateAvailableInfo {
950 current_version: current_version.clone(),
951 latest_version: latest_version.clone(),
952 });
953 }
954 crate::scheduler::UpdateNotification::CheckFailed { error } => {
955 tracing::warn!("Update check failed: {}", error);
956 }
957 crate::scheduler::UpdateNotification::Stopped => {
958 break;
959 }
960 }
961 }
962 });
963
964 Ok(handle)
965}
966
967#[derive(Debug, Clone)]
969pub struct UpdateAvailableInfo {
970 pub current_version: String,
971 pub latest_version: String,
972}
973
974pub fn backup_binary(binary_path: &Path, version: &str) -> Result<PathBuf> {
997 info!(
998 "Backing up binary at {:?} with version {}",
999 binary_path, version
1000 );
1001
1002 if !binary_path.exists() {
1003 anyhow::bail!("Binary not found at {:?}", binary_path);
1004 }
1005
1006 let backup_path = binary_path.with_extension(format!("bak-{}", version));
1007
1008 fs::copy(binary_path, &backup_path)?;
1009
1010 info!("Backup created at {:?}", backup_path);
1011 Ok(backup_path)
1012}
1013
1014pub fn rollback(backup_path: &Path, target_path: &Path) -> Result<()> {
1038 info!("Rolling back from {:?} to {:?}", backup_path, target_path);
1039
1040 if !backup_path.exists() {
1041 anyhow::bail!("Backup not found at {:?}", backup_path);
1042 }
1043
1044 fs::copy(backup_path, target_path)?;
1045
1046 info!("Rollback completed successfully");
1047 Ok(())
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::*;
1053 use std::io::Write;
1054 use tempfile::NamedTempFile;
1055
1056 #[test]
1057 fn test_version_comparison() {
1058 let config = UpdaterConfig::new("test");
1059 let updater = TerraphimUpdater::new(config);
1060
1061 assert!(updater.is_newer_version("1.1.0", "1.0.0").unwrap());
1063 assert!(updater.is_newer_version("2.0.0", "1.9.9").unwrap());
1064 assert!(updater.is_newer_version("1.0.1", "1.0.0").unwrap());
1065
1066 assert!(!updater.is_newer_version("1.0.0", "1.0.0").unwrap());
1068
1069 assert!(!updater.is_newer_version("1.0.0", "1.1.0").unwrap());
1071 assert!(!updater.is_newer_version("1.9.9", "2.0.0").unwrap());
1072
1073 assert!(updater.is_newer_version("v1.1.0", "v1.0.0").unwrap());
1075 assert!(updater.is_newer_version("1.1.0", "v1.0.0").unwrap());
1076 assert!(updater.is_newer_version("v1.1.0", "1.0.0").unwrap());
1077 }
1078
1079 #[tokio::test]
1080 async fn test_updater_config() {
1081 let config = UpdaterConfig::new("test-binary")
1082 .with_version("1.0.0")
1083 .with_progress(false);
1084
1085 assert_eq!(config.bin_name, "test-binary");
1086 assert_eq!(config.current_version, "1.0.0");
1087 assert!(!config.show_progress);
1088 assert_eq!(config.repo_owner, "terraphim");
1089 assert_eq!(config.repo_name, "terraphim-ai");
1090 }
1091
1092 #[test]
1093 fn test_backup_binary() {
1094 let mut temp_file = NamedTempFile::new().unwrap();
1096 writeln!(temp_file, "test binary content").unwrap();
1097
1098 let binary_path = temp_file.path();
1099 let version = "1.0.0";
1100
1101 let backup_path = backup_binary(binary_path, version).unwrap();
1102
1103 assert!(backup_path.exists());
1105 assert!(backup_path.to_string_lossy().contains("bak-1.0.0"));
1106
1107 let original_content = fs::read_to_string(binary_path).unwrap();
1109 let backup_content = fs::read_to_string(&backup_path).unwrap();
1110 assert_eq!(original_content, backup_content);
1111
1112 fs::remove_file(&backup_path).unwrap();
1114 }
1115
1116 #[test]
1117 fn test_backup_binary_nonexistent() {
1118 let nonexistent_path = Path::new("/nonexistent/path/to/binary");
1119
1120 let result = backup_binary(nonexistent_path, "1.0.0");
1121 assert!(result.is_err());
1122 }
1123
1124 #[test]
1125 fn test_rollback() {
1126 let mut backup_file = NamedTempFile::new().unwrap();
1128 writeln!(backup_file, "backup content").unwrap();
1129
1130 let backup_path = backup_file.path();
1131
1132 let mut target_file = NamedTempFile::new().unwrap();
1134 writeln!(target_file, "original content").unwrap();
1135 let target_path = target_file.path();
1136
1137 rollback(backup_path, target_path).unwrap();
1139
1140 let target_content = fs::read_to_string(target_path).unwrap();
1142 assert_eq!(target_content, "backup content\n");
1143 }
1144
1145 #[test]
1146 fn test_rollback_nonexistent() {
1147 let nonexistent_backup = Path::new("/nonexistent/backup.bak");
1148 let temp_file = NamedTempFile::new().unwrap();
1149 let target_path = temp_file.path();
1150
1151 let result = rollback(nonexistent_backup, target_path);
1152 assert!(result.is_err());
1153 }
1154
1155 #[test]
1156 fn test_backup_and_rollback_roundtrip() {
1157 let mut original_file = NamedTempFile::new().unwrap();
1159 writeln!(original_file, "original binary v1.0.0").unwrap();
1160 let original_path = original_file.path();
1161
1162 let backup_path = backup_binary(original_path, "1.0.0").unwrap();
1164
1165 fs::write(original_path, "updated binary v1.1.0").unwrap();
1167
1168 assert_eq!(
1170 fs::read_to_string(original_path).unwrap(),
1171 "updated binary v1.1.0"
1172 );
1173
1174 rollback(&backup_path, original_path).unwrap();
1176
1177 assert_eq!(
1179 fs::read_to_string(original_path).unwrap(),
1180 "original binary v1.0.0\n"
1181 );
1182
1183 fs::remove_file(&backup_path).unwrap();
1185 }
1186
1187 #[tokio::test]
1188 async fn test_check_for_updates_auto() {
1189 let status = check_for_updates_auto("terraphim", "0.0.1").await;
1192
1193 match status {
1194 Ok(UpdateStatus::Available {
1195 current_version,
1196 latest_version,
1197 }) => {
1198 assert_eq!(current_version, "0.0.1");
1199 assert_ne!(current_version, latest_version);
1200 }
1201 Ok(UpdateStatus::UpToDate(version)) => {
1202 assert_eq!(version, "0.0.1");
1203 }
1204 Ok(UpdateStatus::Failed(_)) => {
1205 }
1207 _ => {}
1208 }
1209 }
1210
1211 #[test]
1212 fn test_is_newer_version_static() {
1213 assert!(is_newer_version_static("2.0.0", "1.0.0").unwrap());
1215 assert!(is_newer_version_static("1.1.0", "1.0.0").unwrap());
1216 assert!(is_newer_version_static("1.0.1", "1.0.0").unwrap());
1217
1218 assert!(!is_newer_version_static("1.0.0", "1.0.0").unwrap());
1220
1221 assert!(!is_newer_version_static("1.0.0", "2.0.0").unwrap());
1223 assert!(!is_newer_version_static("1.0.0", "1.1.0").unwrap());
1224
1225 assert!(is_newer_version_static("v2.0.0", "v1.0.0").unwrap());
1227 assert!(!is_newer_version_static("v1.0.0", "v2.0.0").unwrap());
1228 }
1229
1230 #[test]
1231 fn test_update_status_display() {
1232 let up_to_date = UpdateStatus::UpToDate("1.0.0".to_string());
1233 assert!(up_to_date.to_string().contains("1.0.0"));
1234
1235 let updated = UpdateStatus::Updated {
1236 from_version: "1.0.0".to_string(),
1237 to_version: "2.0.0".to_string(),
1238 };
1239 assert!(updated.to_string().contains("1.0.0"));
1240 assert!(updated.to_string().contains("2.0.0"));
1241
1242 let available = UpdateStatus::Available {
1243 current_version: "1.0.0".to_string(),
1244 latest_version: "2.0.0".to_string(),
1245 };
1246 assert!(available.to_string().contains("1.0.0"));
1247 assert!(available.to_string().contains("2.0.0"));
1248
1249 let failed = UpdateStatus::Failed("test error".to_string());
1250 assert!(failed.to_string().contains("test error"));
1251 }
1252}