cloudpub_client/upgrade/
mod.rs

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