1use crate::config::ClientConfig;
2use crate::shell::get_cache_dir;
3use anyhow::{Context, Result};
4use cloudpub_common::protocol::message::Message;
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::mpsc;
10
11#[cfg(unix)]
13const UNIX_SERVICE_UPDATE_SCRIPT: &str = include_str!("scripts/unix_service_update.sh");
14#[cfg(unix)]
15const UNIX_CLIENT_UPDATE_SCRIPT: &str = include_str!("scripts/unix_client_update.sh");
16#[cfg(windows)]
17const WINDOWS_SERVICE_UPDATE_SCRIPT: &str = include_str!("scripts/windows_service_update.bat");
18#[cfg(windows)]
19const WINDOWS_CLIENT_UPDATE_SCRIPT: &str = include_str!("scripts/windows_client_update.bat");
20#[cfg(windows)]
21const WINDOWS_MSI_INSTALL_SCRIPT: &str = include_str!("scripts/windows_msi_install.bat");
22#[cfg(target_os = "macos")]
23const MACOS_DMG_INSTALL_SCRIPT: &str = include_str!("scripts/macos_dmg_install.sh");
24#[cfg(target_os = "linux")]
25const LINUX_DEB_INSTALL_SCRIPT: &str = include_str!("scripts/linux_deb_install.sh");
26#[cfg(target_os = "linux")]
27const LINUX_RPM_INSTALL_SCRIPT: &str = include_str!("scripts/linux_rpm_install.sh");
28#[cfg(target_os = "linux")]
29const SYSTEMD_ONESHOT_TEMPLATE: &str = include_str!("scripts/systemd_oneshot.service");
30#[cfg(target_os = "macos")]
31const LAUNCHD_ONESHOT_TEMPLATE: &str = include_str!("scripts/launchd_oneshot.plist");
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PlatformDownloads {
35 pub gui: Option<String>,
36 pub cli: Option<String>,
37 pub rpm: Option<String>,
38 pub deb: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct OsDownloads {
43 pub name: String,
44 pub platforms: HashMap<String, PlatformDownloads>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DownloadConfig {
49 pub macos: OsDownloads,
50 pub windows: OsDownloads,
51 pub linux: OsDownloads,
52 pub android: OsDownloads,
53}
54
55impl DownloadConfig {
56 pub fn new(base_url: &str, version: &str, branch: &str) -> Self {
57 #[cfg(target_os = "macos")]
58 let macos_platforms = {
59 let mut platforms = HashMap::new();
60 platforms.insert(
61 "aarch64".to_string(),
62 PlatformDownloads {
63 gui: Some(format!(
64 "{base_url}cloudpub-{version}-{branch}-macos-aarch64.dmg"
65 )),
66 cli: Some(format!(
67 "{base_url}clo-{version}-{branch}-macos-aarch64.tar.gz"
68 )),
69 rpm: None,
70 deb: None,
71 },
72 );
73 platforms.insert(
74 "x86_64".to_string(),
75 PlatformDownloads {
76 gui: Some(format!(
77 "{base_url}cloudpub-{version}-{branch}-macos-x86_64.dmg"
78 )),
79 cli: Some(format!(
80 "{base_url}clo-{version}-{branch}-macos-x86_64.tar.gz"
81 )),
82 rpm: None,
83 deb: None,
84 },
85 );
86 platforms
87 };
88
89 #[cfg(target_os = "windows")]
90 let windows_platforms = {
91 let mut platforms = HashMap::new();
92 platforms.insert(
93 "x86_64".to_string(),
94 PlatformDownloads {
95 gui: Some(format!(
96 "{base_url}cloudpub-{version}-{branch}-windows-x86_64.msi"
97 )),
98 cli: Some(format!(
99 "{base_url}clo-{version}-{branch}-windows-x86_64.zip"
100 )),
101 rpm: None,
102 deb: None,
103 },
104 );
105 platforms
106 };
107
108 #[cfg(target_os = "linux")]
109 let linux_platforms = {
110 let mut platforms = HashMap::new();
111 platforms.insert(
112 "arm".to_string(),
113 PlatformDownloads {
114 gui: None,
115 cli: Some(format!("{base_url}clo-{version}-{branch}-linux-arm.tar.gz")),
116 rpm: None,
117 deb: None,
118 },
119 );
120 platforms.insert(
121 "armv5te".to_string(),
122 PlatformDownloads {
123 gui: None,
124 cli: Some(format!(
125 "{base_url}clo-{version}-{branch}-linux-armv5te.tar.gz"
126 )),
127 rpm: None,
128 deb: None,
129 },
130 );
131 platforms.insert(
132 "aarch64".to_string(),
133 PlatformDownloads {
134 gui: None,
135 cli: Some(format!(
136 "{base_url}clo-{version}-{branch}-linux-aarch64.tar.gz"
137 )),
138 rpm: None,
139 deb: None,
140 },
141 );
142 platforms.insert(
143 "mips".to_string(),
144 PlatformDownloads {
145 gui: None,
146 cli: Some(format!(
147 "{base_url}clo-{version}-{branch}-linux-mips.tar.gz"
148 )),
149 rpm: None,
150 deb: None,
151 },
152 );
153 platforms.insert(
154 "mipsel".to_string(),
155 PlatformDownloads {
156 gui: None,
157 cli: Some(format!(
158 "{base_url}clo-{version}-{branch}-linux-mipsel.tar.gz"
159 )),
160 rpm: None,
161 deb: None,
162 },
163 );
164 platforms.insert(
165 "i686".to_string(),
166 PlatformDownloads {
167 gui: None,
168 cli: Some(format!(
169 "{base_url}clo-{version}-{branch}-linux-i686.tar.gz"
170 )),
171 rpm: None,
172 deb: None,
173 },
174 );
175 platforms.insert(
176 "x86_64".to_string(),
177 PlatformDownloads {
178 gui: None,
179 cli: Some(format!(
180 "{base_url}clo-{version}-{branch}-linux-x86_64.tar.gz"
181 )),
182 rpm: Some(format!(
183 "{base_url}cloudpub-{version}-{branch}-linux-x86_64.rpm"
184 )),
185 deb: Some(format!(
186 "{base_url}cloudpub-{version}-{branch}-linux-x86_64.deb"
187 )),
188 },
189 );
190 platforms
191 };
192
193 #[cfg(target_os = "android")]
194 let android_platforms = {
195 let mut platforms = HashMap::new();
196 platforms.insert(
197 "aarch64".to_string(),
198 PlatformDownloads {
199 gui: None,
200 cli: Some(format!(
201 "{base_url}clo-{version}-{branch}-android-arm64.tar.gz"
202 )),
203 rpm: None,
204 deb: None,
205 },
206 );
207 platforms
208 };
209
210 Self {
211 #[cfg(target_os = "macos")]
212 macos: OsDownloads {
213 name: "macOS".to_string(),
214 platforms: macos_platforms,
215 },
216 #[cfg(not(target_os = "macos"))]
217 macos: OsDownloads {
218 name: "macOS".to_string(),
219 platforms: HashMap::new(),
220 },
221 #[cfg(target_os = "windows")]
222 windows: OsDownloads {
223 name: "Windows".to_string(),
224 platforms: windows_platforms,
225 },
226 #[cfg(not(target_os = "windows"))]
227 windows: OsDownloads {
228 name: "Windows".to_string(),
229 platforms: HashMap::new(),
230 },
231 #[cfg(target_os = "linux")]
232 linux: OsDownloads {
233 name: "Linux".to_string(),
234 platforms: linux_platforms,
235 },
236 #[cfg(not(target_os = "linux"))]
237 linux: OsDownloads {
238 name: "Linux".to_string(),
239 platforms: HashMap::new(),
240 },
241 #[cfg(target_os = "android")]
242 android: OsDownloads {
243 name: "Android".to_string(),
244 platforms: android_platforms,
245 },
246 #[cfg(not(target_os = "android"))]
247 android: OsDownloads {
248 name: "Android".to_string(),
249 platforms: HashMap::new(),
250 },
251 }
252 }
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub enum DownloadType {
257 Gui,
258 Cli,
259 Rpm,
260 Deb,
261}
262
263pub fn get_current_os() -> String {
264 #[cfg(target_os = "macos")]
265 return "macos".to_string();
266 #[cfg(target_os = "windows")]
267 return "windows".to_string();
268 #[cfg(target_os = "linux")]
269 return "linux".to_string();
270 #[cfg(target_os = "android")]
271 return "android".to_string();
272 #[cfg(not(any(
273 target_os = "macos",
274 target_os = "windows",
275 target_os = "linux",
276 target_os = "android"
277 )))]
278 return std::env::consts::OS.to_string();
279}
280
281pub fn get_current_arch() -> String {
282 #[cfg(target_arch = "x86_64")]
283 return "x86_64".to_string();
284 #[cfg(target_arch = "aarch64")]
285 return "aarch64".to_string();
286 #[cfg(target_arch = "arm")]
287 return "arm".to_string();
288 #[cfg(target_arch = "x86")]
289 return "i686".to_string();
290 #[cfg(not(any(
291 target_arch = "x86_64",
292 target_arch = "aarch64",
293 target_arch = "arm",
294 target_arch = "x86"
295 )))]
296 return std::env::consts::ARCH.to_string();
297}
298
299#[cfg(target_os = "linux")]
300pub fn get_linux_package_type() -> Result<String> {
301 if std::process::Command::new("which")
303 .arg("dpkg")
304 .output()
305 .map(|output| output.status.success())
306 .unwrap_or(false)
307 {
308 return Ok("deb".to_string());
309 }
310
311 if std::process::Command::new("which")
313 .arg("rpm")
314 .output()
315 .map(|output| output.status.success())
316 .unwrap_or(false)
317 {
318 return Ok("rpm".to_string());
319 }
320
321 Err(anyhow::anyhow!("Could not detect package type"))
322}
323
324pub fn get_download_url(
325 base_url: &str,
326 download_type: &DownloadType,
327 version: &str,
328 branch: &str,
329) -> Result<String> {
330 let base_url = format!("{}download/{}/", base_url, branch);
331 let config = DownloadConfig::new(&base_url, version, branch);
332 let os = get_current_os();
333 let arch = get_current_arch();
334
335 let os_downloads = match os.as_str() {
336 "macos" => &config.macos,
337 "windows" => &config.windows,
338 "linux" => &config.linux,
339 "android" => &config.android,
340 _ => {
341 return Err(anyhow::anyhow!("Unsupported OS: {}", os))
342 .context("Failed to get OS downloads")
343 }
344 };
345
346 let platform_downloads = os_downloads
347 .platforms
348 .get(&arch)
349 .with_context(|| format!("Unsupported architecture {} for OS {}", arch, os))?;
350
351 let download_url = match download_type {
352 DownloadType::Gui => platform_downloads
353 .gui
354 .as_ref()
355 .context("GUI download not available for this platform")?,
356 DownloadType::Cli => platform_downloads
357 .cli
358 .as_ref()
359 .context("CLI download not available for this platform")?,
360 DownloadType::Rpm => platform_downloads
361 .rpm
362 .as_ref()
363 .context("RPM download not available for this platform")?,
364 DownloadType::Deb => platform_downloads
365 .deb
366 .as_ref()
367 .context("DEB download not available for this platform")?,
368 };
369
370 Ok(download_url.clone())
371}
372
373#[allow(unused_variables)]
375fn run_script(
376 script_name: &str,
377 script_template: &str,
378 replacements: Vec<(&str, String)>,
379 is_service: bool,
380) -> Result<()> {
381 use anyhow::Context;
382 use tracing::{debug, info};
383
384 let cache_dir = get_cache_dir("updates")?;
386 std::fs::create_dir_all(&cache_dir).context("Failed to create updates cache directory")?;
387
388 let timestamp = format!(
389 "{}_{}",
390 std::process::id(),
391 std::time::SystemTime::now()
392 .duration_since(std::time::UNIX_EPOCH)
393 .unwrap_or_default()
394 .as_secs()
395 );
396
397 #[cfg(windows)]
398 let script_path = cache_dir.join(format!("{}_{}.bat", script_name, timestamp));
399 #[cfg(unix)]
400 let script_path = cache_dir.join(format!("{}_{}.sh", script_name, timestamp));
401
402 debug!("Creating script: {}", script_path.display());
403
404 let mut script_content = script_template.to_string();
406 for (placeholder, value) in replacements {
407 script_content = script_content.replace(placeholder, &value);
408 }
409
410 std::fs::write(&script_path, script_content).context("Failed to create script")?;
411
412 info!("Script created at: {}", script_path.display());
413
414 #[cfg(unix)]
416 {
417 use std::os::unix::fs::PermissionsExt;
418 let mut perms = std::fs::metadata(&script_path)?.permissions();
419 perms.set_mode(0o755);
420 std::fs::set_permissions(&script_path, perms)?;
421 }
422
423 #[cfg(windows)]
425 {
426 use std::os::windows::process::CommandExt;
427 std::process::Command::new("cmd")
428 .args(&["/C", &script_path.to_string_lossy()])
429 .creation_flags(0x08000000) .spawn()
431 .context("Failed to execute script")?;
432 }
433
434 #[cfg(target_os = "linux")]
435 {
436 if is_service {
437 let service_name = format!("cloudpub-upgrade-{}.service", timestamp);
439 let service_path = format!("/etc/systemd/system/{}", service_name);
440
441 debug!("Creating systemd one-shot service: {}", service_name);
442
443 let service_content = SYSTEMD_ONESHOT_TEMPLATE
444 .replace("{TIMESTAMP}", ×tamp)
445 .replace("{SCRIPT_PATH}", &script_path.display().to_string());
446
447 std::fs::write(&service_path, service_content)
448 .context("Failed to create systemd one-shot service")?;
449
450 std::process::Command::new("systemctl")
451 .arg("daemon-reload")
452 .output()
453 .context("Failed to reload systemd")?;
454
455 std::process::Command::new("systemctl")
456 .arg("start")
457 .arg(&service_name)
458 .spawn()
459 .context("Failed to start upgrade service")?;
460
461 info!(
462 "Upgrade scheduled via systemd one-shot service: {}",
463 service_name
464 );
465 } else {
466 debug!("Executing script with nohup: {}", script_path.display());
468
469 std::process::Command::new("nohup")
470 .arg("/bin/bash")
471 .arg(&script_path)
472 .stdin(std::process::Stdio::null())
473 .stdout(std::process::Stdio::null())
474 .stderr(std::process::Stdio::null())
475 .spawn()
476 .context("Failed to spawn script with nohup")?;
477
478 info!("Script spawned with nohup");
479 }
480 }
481
482 #[cfg(target_os = "macos")]
483 {
484 if is_service {
485 let plist_name = format!("com.cloudpub.upgrade.{}.plist", timestamp);
487 let plist_path = format!("/Library/LaunchDaemons/{}", plist_name);
488
489 debug!("Creating launchd one-shot job: {}", plist_name);
490
491 let plist_content = LAUNCHD_ONESHOT_TEMPLATE
492 .replace("{TIMESTAMP}", ×tamp)
493 .replace("{SCRIPT_PATH}", &script_path.display().to_string());
494
495 std::fs::write(&plist_path, plist_content)
496 .context("Failed to create launchd one-shot plist")?;
497
498 std::process::Command::new("chmod")
499 .args(["644", &plist_path])
500 .output()
501 .context("Failed to set plist permissions")?;
502
503 std::process::Command::new("launchctl")
504 .args(["load", &plist_path])
505 .spawn()
506 .context("Failed to load upgrade job")?;
507
508 info!("Upgrade scheduled via launchd one-shot job: {}", plist_name);
509 } else {
510 debug!("Executing script with nohup: {}", script_path.display());
512
513 std::process::Command::new("nohup")
514 .arg("/bin/bash")
515 .arg(&script_path)
516 .stdin(std::process::Stdio::null())
517 .stdout(std::process::Stdio::null())
518 .stderr(std::process::Stdio::null())
519 .spawn()
520 .context("Failed to spawn script with nohup")?;
521
522 info!("Script spawned with nohup");
523 }
524 }
525
526 info!("Script execution initiated, exiting current process");
527 std::process::exit(0);
528}
529
530fn run_upgrade_script(
532 script_template: &str,
533 new_exe_path: &std::path::Path,
534 current_args: Vec<String>,
535 is_service: bool,
536) -> Result<()> {
537 use tracing::debug;
538
539 let current_exe = std::env::current_exe()?;
540 let cache_dir = get_cache_dir("updates")?;
541 std::fs::create_dir_all(&cache_dir).context("Failed to create updates cache directory")?;
542
543 let timestamp = format!(
544 "{}_{}",
545 std::process::id(),
546 std::time::SystemTime::now()
547 .duration_since(std::time::UNIX_EPOCH)
548 .unwrap_or_default()
549 .as_secs()
550 );
551
552 let log_file = cache_dir.join(format!("cloudpub_update_{}.log", timestamp));
553
554 debug!("Starting update process");
555 debug!("Current executable: {}", current_exe.display());
556 debug!("New executable: {}", new_exe_path.display());
557 debug!("Current args: {:?}", current_args);
558 debug!("Log file location: {}", log_file.display());
559 debug!("Is service: {}", is_service);
560
561 let config_path = if is_service {
564 current_args
565 .windows(2)
566 .find(|w| w[0] == "--conf" || w[0] == "--config")
567 .and_then(|w| w.get(1))
568 .map(|s| s.as_str())
569 .unwrap_or("")
570 .to_string()
571 } else {
572 String::new()
573 };
574
575 let replacements = vec![
577 ("{LOG_FILE}", log_file.display().to_string()),
578 ("{NEW_EXE}", new_exe_path.display().to_string()),
579 ("{CURRENT_EXE}", current_exe.display().to_string()),
580 ("{ARGS}", current_args.join(" ")),
581 ("{CONFIG_PATH}", config_path),
582 ("{SERVICE_ARGS}", current_args.join(" ")),
583 ];
584
585 run_script("cloudpub_update", script_template, replacements, is_service)
586}
587
588#[cfg(windows)]
589pub fn install_msi_package(msi_path: &std::path::Path) -> Result<()> {
590 let current_exe = std::env::current_exe()?;
591
592 run_script(
593 "cloudpub_install_msi",
594 WINDOWS_MSI_INSTALL_SCRIPT,
595 vec![
596 ("{MSI_PATH}", msi_path.display().to_string()),
597 ("{CURRENT_EXE}", current_exe.display().to_string()),
598 ],
599 false,
600 )
601}
602
603#[cfg(windows)]
604pub fn replace_and_restart_windows(
605 new_exe_path: &std::path::Path,
606 current_args: Vec<String>,
607) -> Result<()> {
608 let is_service = current_args.contains(&"--run-as-service".to_string());
609 let script_template = if is_service {
610 WINDOWS_SERVICE_UPDATE_SCRIPT
611 } else {
612 WINDOWS_CLIENT_UPDATE_SCRIPT
613 };
614 run_upgrade_script(script_template, new_exe_path, current_args, is_service)
615}
616
617#[cfg(not(windows))]
618pub fn replace_and_restart_unix(
619 new_exe_path: &std::path::Path,
620 current_args: Vec<String>,
621) -> Result<()> {
622 let is_service = current_args.contains(&"--run-as-service".to_string());
623 let script_template = if is_service {
624 UNIX_SERVICE_UPDATE_SCRIPT
625 } else {
626 UNIX_CLIENT_UPDATE_SCRIPT
627 };
628 run_upgrade_script(script_template, new_exe_path, current_args, is_service)
629}
630
631pub fn apply_update_and_restart(new_exe_path: &std::path::Path) -> Result<()> {
632 let args: Vec<String> = std::env::args().skip(1).collect();
634
635 eprintln!("{}", crate::t!("applying-update"));
636
637 #[cfg(windows)]
638 replace_and_restart_windows(new_exe_path, args)?;
639
640 #[cfg(not(windows))]
641 replace_and_restart_unix(new_exe_path, args)?;
642
643 Ok(())
644}
645
646#[cfg(target_os = "macos")]
647async fn install_dmg_package(package_path: &std::path::Path) -> Result<()> {
648 eprintln!("Installing DMG package...");
649
650 let current_exe = std::env::current_exe()?;
651 let current_args: Vec<String> = std::env::args().skip(1).collect();
652
653 run_script(
654 "cloudpub_install_dmg",
655 MACOS_DMG_INSTALL_SCRIPT,
656 vec![
657 ("{PACKAGE_PATH}", package_path.display().to_string()),
658 ("{CURRENT_EXE}", current_exe.display().to_string()),
659 ("{CURRENT_ARGS}", current_args.join(" ")),
660 ],
661 false,
662 )
663}
664
665#[cfg(target_os = "linux")]
666async fn install_deb_package(package_path: &std::path::Path) -> Result<()> {
667 eprintln!("Installing DEB package...");
668
669 let current_exe = std::env::current_exe()?;
670 let current_args: Vec<String> = std::env::args().skip(1).collect();
671
672 run_script(
673 "cloudpub_install_deb",
674 LINUX_DEB_INSTALL_SCRIPT,
675 vec![
676 ("{PACKAGE_PATH}", package_path.display().to_string()),
677 ("{CURRENT_EXE}", current_exe.display().to_string()),
678 ("{CURRENT_ARGS}", current_args.join(" ")),
679 ],
680 false,
681 )
682}
683
684#[cfg(target_os = "linux")]
685async fn install_rpm_package(package_path: &std::path::Path) -> Result<()> {
686 eprintln!("Installing RPM package...");
687
688 let current_exe = std::env::current_exe()?;
689 let current_args: Vec<String> = std::env::args().skip(1).collect();
690
691 run_script(
692 "cloudpub_install_rpm",
693 LINUX_RPM_INSTALL_SCRIPT,
694 vec![
695 ("{PACKAGE_PATH}", package_path.display().to_string()),
696 ("{CURRENT_EXE}", current_exe.display().to_string()),
697 ("{CURRENT_ARGS}", current_args.join(" ")),
698 ],
699 false,
700 )
701}
702
703async fn handle_cli_update(
704 filename: &str,
705 output_path: &std::path::Path,
706 cache_dir: &std::path::Path,
707 command_rx: &mut mpsc::Receiver<Message>,
708 result_tx: &mpsc::Sender<Message>,
709) -> Result<()> {
710 let unpack_message = "Unpacking update".to_string();
711
712 if filename.ends_with(".zip") {
713 crate::shell::unzip(&unpack_message, output_path, cache_dir, 0, result_tx)
714 .await
715 .context("Failed to unpack zip update")?;
716 } else if filename.ends_with(".tar.gz") {
717 let tar_args = vec![
718 "-xzf".to_string(),
719 output_path.to_string_lossy().to_string(),
720 ];
721
722 let total_files = 1;
723 let progress = Some((unpack_message, result_tx.clone(), total_files));
724
725 crate::shell::execute(
726 std::path::PathBuf::from("tar"),
727 tar_args,
728 Some(cache_dir.to_path_buf()),
729 std::collections::HashMap::new(),
730 progress,
731 command_rx,
732 )
733 .await
734 .context("Failed to unpack tar.gz update")?;
735 } else {
736 return Err(anyhow::anyhow!("Unknown file format: {}", filename));
737 }
738
739 eprintln!(
740 "{}",
741 crate::t!("update-unpacked", "path" => cache_dir.display().to_string())
742 );
743
744 #[cfg(not(windows))]
745 let new_exe_path = cache_dir.join("clo");
746 #[cfg(windows)]
747 let new_exe_path = cache_dir.join("clo.exe");
748
749 apply_update_and_restart(&new_exe_path)?;
750 Ok(())
751}
752
753pub async fn handle_upgrade_download(
754 version: &str,
755 gui: bool,
756 config: Arc<RwLock<ClientConfig>>,
757 command_rx: &mut mpsc::Receiver<Message>,
758 result_tx: &mpsc::Sender<Message>,
759) -> Result<(std::path::PathBuf, DownloadType, String)> {
760 let cache_dir = get_cache_dir("updates").unwrap();
762 std::fs::remove_dir_all(&cache_dir).ok();
763
764 let cache_dir = get_cache_dir("updates").unwrap();
766
767 let download_type = if gui {
768 #[cfg(target_os = "linux")]
769 {
770 match get_linux_package_type() {
772 Ok(pkg_type) if pkg_type == "deb" => DownloadType::Deb,
773 Ok(pkg_type) if pkg_type == "rpm" => DownloadType::Rpm,
774 _ => DownloadType::Cli, }
776 }
777 #[cfg(not(target_os = "linux"))]
778 DownloadType::Gui
779 } else {
780 DownloadType::Cli
781 };
782
783 let base_url = config.read().server.to_string();
785 let download_url =
786 get_download_url(&base_url, &download_type, version, cloudpub_common::BRANCH)?;
787
788 let mut file_extension = std::path::Path::new(&download_url)
790 .extension()
791 .and_then(|ext| ext.to_str())
792 .map(|ext| format!(".{}", ext))
793 .unwrap_or_else(|| ".tar.gz".to_string());
794
795 if file_extension == ".gz" {
796 file_extension = ".tar.gz".to_string();
798 }
799
800 let filename = format!("cloudpub-{}{}", version, file_extension);
801 let output_path = cache_dir.join(&filename);
802
803 let message = crate::t!("downloading-update", "path" => output_path.display().to_string());
804
805 crate::shell::download(
806 &message,
807 config.clone(),
808 &download_url,
809 &output_path,
810 command_rx,
811 result_tx,
812 )
813 .await
814 .with_context(|| format!("Failed to download update ({})", download_url))?;
815
816 eprintln!(
817 "{}",
818 crate::t!("update-downloaded", "path" => output_path.display().to_string())
819 );
820
821 Ok((output_path, download_type, filename))
822}
823
824pub async fn handle_upgrade_install(
825 output_path: std::path::PathBuf,
826 download_type: DownloadType,
827 filename: String,
828 command_rx: &mut mpsc::Receiver<Message>,
829 result_tx: &mpsc::Sender<Message>,
830) -> Result<()> {
831 let cache_dir = output_path.parent().unwrap();
832 std::env::set_current_dir(cache_dir)
834 .context("Failed to change current directory to cache directory")?;
835
836 match download_type {
838 DownloadType::Gui => {
839 #[cfg(target_os = "macos")]
840 {
841 if filename.ends_with(".dmg") {
842 install_dmg_package(&output_path).await?;
843 } else {
844 return Err(anyhow::anyhow!("Unsupported GUI package format"));
845 }
846 }
847 #[cfg(target_os = "windows")]
848 {
849 if filename.ends_with(".msi") {
850 install_msi_package(&output_path)?;
851 } else {
852 return Err(anyhow::anyhow!("Unsupported GUI package format"));
853 }
854 }
855 #[cfg(target_os = "linux")]
856 {
857 return Err(anyhow::anyhow!("GUI packages not supported on Linux"));
858 }
859 }
860 DownloadType::Deb => {
861 #[cfg(target_os = "linux")]
862 {
863 install_deb_package(&output_path).await?;
864 }
865 #[cfg(not(target_os = "linux"))]
866 {
867 return Err(anyhow::anyhow!("DEB packages only supported on Linux"));
868 }
869 }
870 DownloadType::Rpm => {
871 #[cfg(target_os = "linux")]
872 {
873 install_rpm_package(&output_path).await?;
874 }
875 #[cfg(not(target_os = "linux"))]
876 {
877 return Err(anyhow::anyhow!("RPM packages only supported on Linux"));
878 }
879 }
880 _ => {
881 handle_cli_update(&filename, &output_path, cache_dir, command_rx, result_tx).await?;
883 }
884 }
885
886 Ok(())
887}
888
889pub async fn handle_upgrade_available(
890 version: &str,
891 config: Arc<RwLock<ClientConfig>>,
892 gui: bool,
893 command_rx: &mut mpsc::Receiver<Message>,
894 result_tx: &mpsc::Sender<Message>,
895) -> Result<()> {
896 let (output_path, download_type, filename) =
897 handle_upgrade_download(version, gui, config, command_rx, result_tx).await?;
898
899 handle_upgrade_install(output_path, download_type, filename, command_rx, result_tx).await?;
900
901 Ok(())
902}