cloudpub_client/
upgrade.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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PlatformDownloads {
13    pub gui: Option<String>,
14    pub cli: Option<String>,
15    pub rpm: Option<String>,
16    pub deb: Option<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct OsDownloads {
21    pub name: String,
22    pub platforms: HashMap<String, PlatformDownloads>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DownloadConfig {
27    pub macos: OsDownloads,
28    pub windows: OsDownloads,
29    pub linux: OsDownloads,
30}
31
32impl DownloadConfig {
33    pub fn new(base_url: &str, version: &str, branch: &str) -> Self {
34        #[cfg(target_os = "macos")]
35        let macos_platforms = {
36            let mut platforms = HashMap::new();
37            platforms.insert(
38                "aarch64".to_string(),
39                PlatformDownloads {
40                    gui: Some(format!(
41                        "{base_url}cloudpub-{version}-{branch}-macos-aarch64.dmg"
42                    )),
43                    cli: Some(format!(
44                        "{base_url}clo-{version}-{branch}-macos-aarch64.tar.gz"
45                    )),
46                    rpm: None,
47                    deb: None,
48                },
49            );
50            platforms.insert(
51                "x86_64".to_string(),
52                PlatformDownloads {
53                    gui: Some(format!(
54                        "{base_url}cloudpub-{version}-{branch}-macos-x86_64.dmg"
55                    )),
56                    cli: Some(format!(
57                        "{base_url}clo-{version}-{branch}-macos-x86_64.tar.gz"
58                    )),
59                    rpm: None,
60                    deb: None,
61                },
62            );
63            platforms
64        };
65
66        #[cfg(target_os = "windows")]
67        let windows_platforms = {
68            let mut platforms = HashMap::new();
69            platforms.insert(
70                "x86_64".to_string(),
71                PlatformDownloads {
72                    gui: Some(format!(
73                        "{base_url}cloudpub-{version}-{branch}-windows-x86_64.msi"
74                    )),
75                    cli: Some(format!(
76                        "{base_url}clo-{version}-{branch}-windows-x86_64.zip"
77                    )),
78                    rpm: None,
79                    deb: None,
80                },
81            );
82            platforms
83        };
84
85        #[cfg(target_os = "linux")]
86        let linux_platforms = {
87            let mut platforms = HashMap::new();
88            platforms.insert(
89                "arm".to_string(),
90                PlatformDownloads {
91                    gui: None,
92                    cli: Some(format!("{base_url}clo-{version}-{branch}-linux-arm.tar.gz")),
93                    rpm: None,
94                    deb: None,
95                },
96            );
97            platforms.insert(
98                "armv5te".to_string(),
99                PlatformDownloads {
100                    gui: None,
101                    cli: Some(format!(
102                        "{base_url}clo-{version}-{branch}-linux-armv5te.tar.gz"
103                    )),
104                    rpm: None,
105                    deb: None,
106                },
107            );
108            platforms.insert(
109                "aarch64".to_string(),
110                PlatformDownloads {
111                    gui: None,
112                    cli: Some(format!(
113                        "{base_url}clo-{version}-{branch}-linux-aarch64.tar.gz"
114                    )),
115                    rpm: None,
116                    deb: None,
117                },
118            );
119            platforms.insert(
120                "mips".to_string(),
121                PlatformDownloads {
122                    gui: None,
123                    cli: Some(format!(
124                        "{base_url}clo-{version}-{branch}-linux-mips.tar.gz"
125                    )),
126                    rpm: None,
127                    deb: None,
128                },
129            );
130            platforms.insert(
131                "mipsel".to_string(),
132                PlatformDownloads {
133                    gui: None,
134                    cli: Some(format!(
135                        "{base_url}clo-{version}-{branch}-linux-mipsel.tar.gz"
136                    )),
137                    rpm: None,
138                    deb: None,
139                },
140            );
141            platforms.insert(
142                "i686".to_string(),
143                PlatformDownloads {
144                    gui: None,
145                    cli: Some(format!(
146                        "{base_url}clo-{version}-{branch}-linux-i686.tar.gz"
147                    )),
148                    rpm: None,
149                    deb: None,
150                },
151            );
152            platforms.insert(
153                "x86_64".to_string(),
154                PlatformDownloads {
155                    gui: None,
156                    cli: Some(format!(
157                        "{base_url}clo-{version}-{branch}-linux-x86_64.tar.gz"
158                    )),
159                    rpm: Some(format!(
160                        "{base_url}cloudpub-{version}-{branch}-linux-x86_64.rpm"
161                    )),
162                    deb: Some(format!(
163                        "{base_url}cloudpub-{version}-{branch}-linux-x86_64.deb"
164                    )),
165                },
166            );
167            platforms
168        };
169
170        Self {
171            #[cfg(target_os = "macos")]
172            macos: OsDownloads {
173                name: "macOS".to_string(),
174                platforms: macos_platforms,
175            },
176            #[cfg(not(target_os = "macos"))]
177            macos: OsDownloads {
178                name: "macOS".to_string(),
179                platforms: HashMap::new(),
180            },
181            #[cfg(target_os = "windows")]
182            windows: OsDownloads {
183                name: "Windows".to_string(),
184                platforms: windows_platforms,
185            },
186            #[cfg(not(target_os = "windows"))]
187            windows: OsDownloads {
188                name: "Windows".to_string(),
189                platforms: HashMap::new(),
190            },
191            #[cfg(target_os = "linux")]
192            linux: OsDownloads {
193                name: "Linux".to_string(),
194                platforms: linux_platforms,
195            },
196            #[cfg(not(target_os = "linux"))]
197            linux: OsDownloads {
198                name: "Linux".to_string(),
199                platforms: HashMap::new(),
200            },
201        }
202    }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub enum DownloadType {
207    Gui,
208    Cli,
209    Rpm,
210    Deb,
211}
212
213pub fn get_current_os() -> String {
214    #[cfg(target_os = "macos")]
215    return "macos".to_string();
216    #[cfg(target_os = "windows")]
217    return "windows".to_string();
218    #[cfg(target_os = "linux")]
219    return "linux".to_string();
220    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
221    return std::env::consts::OS.to_string();
222}
223
224pub fn get_current_arch() -> String {
225    #[cfg(target_arch = "x86_64")]
226    return "x86_64".to_string();
227    #[cfg(target_arch = "aarch64")]
228    return "aarch64".to_string();
229    #[cfg(target_arch = "arm")]
230    return "arm".to_string();
231    #[cfg(target_arch = "x86")]
232    return "i686".to_string();
233    #[cfg(not(any(
234        target_arch = "x86_64",
235        target_arch = "aarch64",
236        target_arch = "arm",
237        target_arch = "x86"
238    )))]
239    return std::env::consts::ARCH.to_string();
240}
241
242#[cfg(target_os = "linux")]
243pub fn get_linux_package_type() -> Result<String> {
244    // Check for dpkg (Debian/Ubuntu systems)
245    if std::process::Command::new("which")
246        .arg("dpkg")
247        .output()
248        .map(|output| output.status.success())
249        .unwrap_or(false)
250    {
251        return Ok("deb".to_string());
252    }
253
254    // Check for rpm (Red Hat/SUSE systems)
255    if std::process::Command::new("which")
256        .arg("rpm")
257        .output()
258        .map(|output| output.status.success())
259        .unwrap_or(false)
260    {
261        return Ok("rpm".to_string());
262    }
263
264    Err(anyhow::anyhow!("Could not detect package type"))
265}
266
267pub fn get_download_url(
268    base_url: &str,
269    download_type: &DownloadType,
270    version: &str,
271    branch: &str,
272) -> Result<String> {
273    let base_url = format!("{}download/{}/", base_url, branch);
274    let config = DownloadConfig::new(&base_url, version, branch);
275    let os = get_current_os();
276    let arch = get_current_arch();
277
278    let os_downloads = match os.as_str() {
279        "macos" => &config.macos,
280        "windows" => &config.windows,
281        "linux" => &config.linux,
282        _ => {
283            return Err(anyhow::anyhow!("Unsupported OS: {}", os))
284                .context("Failed to get OS downloads")
285        }
286    };
287
288    let platform_downloads = os_downloads
289        .platforms
290        .get(&arch)
291        .with_context(|| format!("Unsupported architecture {} for OS {}", arch, os))?;
292
293    let download_url = match download_type {
294        DownloadType::Gui => platform_downloads
295            .gui
296            .as_ref()
297            .context("GUI download not available for this platform")?,
298        DownloadType::Cli => platform_downloads
299            .cli
300            .as_ref()
301            .context("CLI download not available for this platform")?,
302        DownloadType::Rpm => platform_downloads
303            .rpm
304            .as_ref()
305            .context("RPM download not available for this platform")?,
306        DownloadType::Deb => platform_downloads
307            .deb
308            .as_ref()
309            .context("DEB download not available for this platform")?,
310    };
311
312    Ok(download_url.clone())
313}
314
315#[cfg(windows)]
316pub fn install_msi_package(msi_path: &std::path::Path) -> Result<()> {
317    use std::os::windows::process::CommandExt;
318
319    let temp_dir = std::env::temp_dir();
320    let batch_script = temp_dir.join("cloudpub_install_msi.bat");
321    let current_exe = std::env::current_exe()?;
322
323    let script_content = format!(
324        r#"@echo off
325timeout /t 3 /nobreak >nul
326echo Installing MSI package...
327echo Requesting administrator privileges...
328powershell -Command "Start-Process msiexec -ArgumentList '/i', '{}', '/quiet', '/norestart' -Verb RunAs -Wait"
329if errorlevel 1 (
330    echo Failed to install MSI package
331    exit /b 1
332)
333echo Installation completed successfully
334del "%~f0"
335start "" "{}"
336"#,
337        msi_path.display(),
338        current_exe.display()
339    );
340
341    std::fs::write(&batch_script, script_content)
342        .context("Failed to create MSI install batch script")?;
343
344    // Execute the batch script and exit current process
345    std::process::Command::new("cmd")
346        .args(&["/C", &batch_script.to_string_lossy()])
347        .creation_flags(0x08000000) // CREATE_NO_WINDOW
348        .spawn()
349        .context("Failed to execute MSI install batch script")?;
350
351    std::process::exit(0);
352}
353
354#[cfg(windows)]
355pub fn replace_and_restart_windows(
356    new_exe_path: &std::path::Path,
357    current_args: Vec<String>,
358) -> Result<()> {
359    use std::os::windows::process::CommandExt;
360
361    let current_exe = std::env::current_exe()?;
362    let temp_dir = std::env::temp_dir();
363    let batch_script = temp_dir.join("cloudpub_update.bat");
364
365    let script_content = format!(
366        r#"@echo off
367timeout /t 3 /nobreak >nul
368move /y "{}" "{}"
369if errorlevel 1 (
370    echo Failed to replace executable
371    exit /b 1
372)
373start "" "{}" {}
374del "%~f0"
375"#,
376        new_exe_path.display(),
377        current_exe.display(),
378        current_exe.display(),
379        current_args.join(" ")
380    );
381
382    std::fs::write(&batch_script, script_content)
383        .context("Failed to create update batch script")?;
384
385    // Execute the batch script and exit current process
386    std::process::Command::new("cmd")
387        .args(&["/C", &batch_script.to_string_lossy()])
388        .creation_flags(0x08000000) // CREATE_NO_WINDOW
389        .spawn()
390        .context("Failed to execute update batch script")?;
391
392    std::process::exit(0);
393}
394
395#[cfg(not(windows))]
396pub fn replace_and_restart_unix(
397    new_exe_path: &std::path::Path,
398    current_args: Vec<String>,
399) -> Result<()> {
400    let current_exe = std::env::current_exe()?;
401    let backup_exe = current_exe.with_extension("old");
402
403    // Move current executable to backup (this works even if it's running)
404    std::fs::rename(&current_exe, &backup_exe).context("Failed to backup current executable")?;
405
406    // Move new executable to current location (atomic operation)
407    std::fs::rename(new_exe_path, &current_exe).context("Failed to move new executable")?;
408
409    // Set executable permissions
410    #[cfg(unix)]
411    {
412        use std::os::unix::fs::PermissionsExt;
413        let mut perms = std::fs::metadata(&current_exe)?.permissions();
414        perms.set_mode(0o755);
415        std::fs::set_permissions(&current_exe, perms)?;
416    }
417
418    // Clean up backup file (optional, ignore errors)
419    std::fs::remove_file(&backup_exe).ok();
420
421    // Restart the application
422    std::process::Command::new(&current_exe)
423        .args(current_args)
424        .spawn()
425        .context("Failed to restart application")?;
426
427    std::process::exit(0);
428}
429
430pub fn apply_update_and_restart(new_exe_path: &std::path::Path) -> Result<()> {
431    // Get current command line arguments (skip the program name)
432    let args: Vec<String> = vec!["run".to_string()];
433
434    eprintln!("{}", crate::t!("applying-update"));
435
436    #[cfg(windows)]
437    replace_and_restart_windows(new_exe_path, args)?;
438
439    #[cfg(not(windows))]
440    replace_and_restart_unix(new_exe_path, args)?;
441
442    Ok(())
443}
444
445#[cfg(target_os = "macos")]
446async fn install_dmg_package(package_path: &std::path::Path) -> Result<()> {
447    eprintln!("Installing DMG package...");
448
449    // Get current executable and args for restart
450    let current_exe = std::env::current_exe()?;
451    let current_args: Vec<String> = std::env::args().skip(1).collect();
452    let temp_dir = std::env::temp_dir();
453    let install_script = temp_dir.join("cloudpub_install_dmg.sh");
454
455    let script_content = format!(
456        r#"#!/bin/bash
457set -e
458
459PACKAGE_PATH="{}"
460CURRENT_EXE="{}"
461CURRENT_ARGS="{}"
462
463echo "Installing DMG package..."
464
465# Mount the DMG
466MOUNT_OUTPUT=$(hdiutil attach -nobrowse -quiet "$PACKAGE_PATH")
467if [ $? -ne 0 ]; then
468    echo "Failed to mount DMG package"
469    exit 1
470fi
471
472# Extract mount point
473MOUNT_POINT=$(echo "$MOUNT_OUTPUT" | grep "/Volumes/" | awk '{{print $NF}}')
474if [ -z "$MOUNT_POINT" ]; then
475    echo "Could not find mount point"
476    exit 1
477fi
478
479# Find .app bundle
480APP_PATH=$(find "$MOUNT_POINT" -name "*.app" -type d | head -n 1)
481if [ -z "$APP_PATH" ]; then
482    hdiutil detach "$MOUNT_POINT" -quiet
483    echo "No .app bundle found in DMG"
484    exit 1
485fi
486
487APP_NAME=$(basename "$APP_PATH")
488DESTINATION="/Applications/$APP_NAME"
489
490# Remove existing app if it exists
491if [ -d "$DESTINATION" ]; then
492    rm -rf "$DESTINATION"
493fi
494
495# Copy app to Applications
496cp -R "$APP_PATH" "/Applications/"
497if [ $? -ne 0 ]; then
498    hdiutil detach "$MOUNT_POINT" -quiet
499    echo "Failed to copy application"
500    exit 1
501fi
502
503# Unmount DMG
504hdiutil detach "$MOUNT_POINT" -quiet
505
506echo "DMG package installed successfully"
507
508# Launch new version
509APP_NAME_NO_EXT=$(basename "$APP_NAME" .app)
510open -a "$APP_NAME_NO_EXT"
511echo "Launched new application version"
512
513# Clean up script
514rm -f "$0"
515"#,
516        package_path.display(),
517        current_exe.display(),
518        current_args.join(" ")
519    );
520
521    std::fs::write(&install_script, script_content)?;
522
523    // Make script executable
524    #[cfg(unix)]
525    {
526        use std::os::unix::fs::PermissionsExt;
527        let mut perms = std::fs::metadata(&install_script)?.permissions();
528        perms.set_mode(0o755);
529        std::fs::set_permissions(&install_script, perms)?;
530    }
531
532    // Execute install script in background
533    std::process::Command::new("nohup")
534        .arg(&install_script)
535        .spawn()?;
536
537    std::process::exit(0);
538}
539
540#[cfg(target_os = "linux")]
541async fn install_deb_package(package_path: &std::path::Path) -> Result<()> {
542    eprintln!("Installing DEB package...");
543
544    // Get current executable and args for restart
545    let current_exe = std::env::current_exe()?;
546    let current_args: Vec<String> = std::env::args().skip(1).collect();
547    let temp_dir = std::env::temp_dir();
548    let install_script = temp_dir.join("cloudpub_install_deb.sh");
549
550    let script_content = format!(
551        r#"#!/bin/bash
552set -e
553
554PACKAGE_PATH="{}"
555CURRENT_EXE="{}"
556CURRENT_ARGS="{}"
557
558echo "Installing DEB package..."
559
560# Check if running as root, if not use pkexec
561if [ "$EUID" -ne 0 ]; then
562    echo "Root privileges required for package installation"
563    pkexec dpkg -i "$PACKAGE_PATH"
564else
565    dpkg -i "$PACKAGE_PATH"
566fi
567
568if [ $? -ne 0 ]; then
569    echo "Failed to install DEB package"
570    exit 1
571fi
572
573echo "DEB package installed successfully"
574
575# Wait a moment for the process to exit
576sleep 3
577
578# Restart the application
579exec "$CURRENT_EXE" $CURRENT_ARGS
580"#,
581        package_path.display(),
582        current_exe.display(),
583        current_args.join(" ")
584    );
585
586    std::fs::write(&install_script, script_content)?;
587
588    // Make script executable
589    #[cfg(unix)]
590    {
591        use std::os::unix::fs::PermissionsExt;
592        let mut perms = std::fs::metadata(&install_script)?.permissions();
593        perms.set_mode(0o755);
594        std::fs::set_permissions(&install_script, perms)?;
595    }
596
597    // Execute install script in background
598    std::process::Command::new("nohup")
599        .arg(&install_script)
600        .spawn()?;
601
602    std::process::exit(0);
603}
604
605#[cfg(target_os = "linux")]
606async fn install_rpm_package(package_path: &std::path::Path) -> Result<()> {
607    eprintln!("Installing RPM package...");
608
609    // Get current executable and args for restart
610    let current_exe = std::env::current_exe()?;
611    let current_args: Vec<String> = std::env::args().skip(1).collect();
612    let temp_dir = std::env::temp_dir();
613    let install_script = temp_dir.join("cloudpub_install_rpm.sh");
614
615    let script_content = format!(
616        r#"#!/bin/bash
617set -e
618
619PACKAGE_PATH="{}"
620CURRENT_EXE="{}"
621CURRENT_ARGS="{}"
622
623echo "Installing RPM package..."
624
625# Check if running as root, if not use pkexec
626if [ "$EUID" -ne 0 ]; then
627    echo "Root privileges required for package installation"
628    pkexec rpm -Uvh "$PACKAGE_PATH"
629else
630    rpm -Uvh "$PACKAGE_PATH"
631fi
632
633if [ $? -ne 0 ]; then
634    echo "Failed to install RPM package"
635    exit 1
636fi
637
638echo "RPM package installed successfully"
639
640# Wait a moment for the process to exit
641sleep 3
642
643# Restart the application
644exec "$CURRENT_EXE" $CURRENT_ARGS
645"#,
646        package_path.display(),
647        current_exe.display(),
648        current_args.join(" ")
649    );
650
651    std::fs::write(&install_script, script_content)?;
652
653    // Make script executable
654    #[cfg(unix)]
655    {
656        use std::os::unix::fs::PermissionsExt;
657        let mut perms = std::fs::metadata(&install_script)?.permissions();
658        perms.set_mode(0o755);
659        std::fs::set_permissions(&install_script, perms)?;
660    }
661
662    // Execute install script in background
663    std::process::Command::new("nohup")
664        .arg(&install_script)
665        .spawn()?;
666
667    std::process::exit(0);
668}
669
670async fn handle_cli_update(
671    filename: &str,
672    output_path: &std::path::Path,
673    cache_dir: &std::path::Path,
674    command_rx: &mut mpsc::Receiver<Message>,
675    result_tx: &mpsc::Sender<Message>,
676) -> Result<()> {
677    let unpack_message = "Unpacking update".to_string();
678
679    if filename.ends_with(".zip") {
680        crate::shell::unzip(&unpack_message, output_path, cache_dir, 0, result_tx)
681            .await
682            .context("Failed to unpack zip update")?;
683    } else if filename.ends_with(".tar.gz") {
684        let tar_args = vec![
685            "-xzf".to_string(),
686            output_path.to_string_lossy().to_string(),
687        ];
688
689        let total_files = 1;
690        let progress = Some((unpack_message, result_tx.clone(), total_files));
691
692        crate::shell::execute(
693            std::path::PathBuf::from("tar"),
694            tar_args,
695            Some(cache_dir.to_path_buf()),
696            std::collections::HashMap::new(),
697            progress,
698            command_rx,
699        )
700        .await
701        .context("Failed to unpack tar.gz update")?;
702    } else {
703        return Err(anyhow::anyhow!("Unknown file format: {}", filename));
704    }
705
706    eprintln!(
707        "{}",
708        crate::t!("update-unpacked", "path" => cache_dir.display().to_string())
709    );
710
711    #[cfg(not(windows))]
712    let new_exe_path = cache_dir.join("clo");
713    #[cfg(windows)]
714    let new_exe_path = cache_dir.join("clo.exe");
715
716    apply_update_and_restart(&new_exe_path)?;
717    Ok(())
718}
719
720pub async fn handle_upgrade_download(
721    version: &str,
722    gui: bool,
723    config: Arc<RwLock<ClientConfig>>,
724    command_rx: &mut mpsc::Receiver<Message>,
725    result_tx: &mpsc::Sender<Message>,
726) -> Result<(std::path::PathBuf, DownloadType, String)> {
727    // Delete old update cache directory
728    let cache_dir = get_cache_dir("updates").unwrap();
729    std::fs::remove_dir_all(&cache_dir).ok();
730
731    // Download the upgrade
732    let cache_dir = get_cache_dir("updates").unwrap();
733
734    let download_type = if gui {
735        #[cfg(target_os = "linux")]
736        {
737            // Try to detect package type and use appropriate installer
738            match get_linux_package_type() {
739                Ok(pkg_type) if pkg_type == "deb" => DownloadType::Deb,
740                Ok(pkg_type) if pkg_type == "rpm" => DownloadType::Rpm,
741                _ => DownloadType::Cli, // Fallback to CLI
742            }
743        }
744        #[cfg(not(target_os = "linux"))]
745        DownloadType::Gui
746    } else {
747        DownloadType::Cli
748    };
749
750    // Use base URL from config server field
751    let base_url = config.read().server.to_string();
752    let download_url = get_download_url(&base_url, &download_type, version, "stable")?;
753
754    // Extract file extension from download URL
755    let mut file_extension = std::path::Path::new(&download_url)
756        .extension()
757        .and_then(|ext| ext.to_str())
758        .map(|ext| format!(".{}", ext))
759        .unwrap_or_else(|| ".tar.gz".to_string());
760
761    if file_extension == ".gz" {
762        // If the file extension is .gz, we assume it's a tar.gz file
763        file_extension = ".tar.gz".to_string();
764    }
765
766    let filename = format!("cloudpub-{}{}", version, file_extension);
767    let output_path = cache_dir.join(&filename);
768
769    let message = crate::t!("downloading-update", "path" => output_path.display().to_string());
770
771    crate::shell::download(
772        &message,
773        config.clone(),
774        &download_url,
775        &output_path,
776        command_rx,
777        result_tx,
778    )
779    .await
780    .context("Failed to download update")?;
781
782    eprintln!(
783        "{}",
784        crate::t!("update-downloaded", "path" => output_path.display().to_string())
785    );
786
787    Ok((output_path, download_type, filename))
788}
789
790pub async fn handle_upgrade_install(
791    output_path: std::path::PathBuf,
792    download_type: DownloadType,
793    filename: String,
794    command_rx: &mut mpsc::Receiver<Message>,
795    result_tx: &mpsc::Sender<Message>,
796) -> Result<()> {
797    let cache_dir = output_path.parent().unwrap();
798    // change the current directory to the cache directory
799    std::env::set_current_dir(cache_dir)
800        .context("Failed to change current directory to cache directory")?;
801
802    // Handle different package types
803    match download_type {
804        DownloadType::Gui => {
805            #[cfg(target_os = "macos")]
806            {
807                if filename.ends_with(".dmg") {
808                    install_dmg_package(&output_path).await?;
809                } else {
810                    return Err(anyhow::anyhow!("Unsupported GUI package format"));
811                }
812            }
813            #[cfg(target_os = "windows")]
814            {
815                if filename.ends_with(".msi") {
816                    install_msi_package(&output_path)?;
817                } else {
818                    return Err(anyhow::anyhow!("Unsupported GUI package format"));
819                }
820            }
821            #[cfg(target_os = "linux")]
822            {
823                return Err(anyhow::anyhow!("GUI packages not supported on Linux"));
824            }
825        }
826        DownloadType::Deb => {
827            #[cfg(target_os = "linux")]
828            {
829                install_deb_package(&output_path).await?;
830            }
831            #[cfg(not(target_os = "linux"))]
832            {
833                return Err(anyhow::anyhow!("DEB packages only supported on Linux"));
834            }
835        }
836        DownloadType::Rpm => {
837            #[cfg(target_os = "linux")]
838            {
839                install_rpm_package(&output_path).await?;
840            }
841            #[cfg(not(target_os = "linux"))]
842            {
843                return Err(anyhow::anyhow!("RPM packages only supported on Linux"));
844            }
845        }
846        _ => {
847            // Handle CLI updates (tar.gz/zip extraction and replacement)
848            handle_cli_update(&filename, &output_path, cache_dir, command_rx, result_tx).await?;
849        }
850    }
851
852    Ok(())
853}
854
855pub async fn handle_upgrade_available(
856    version: &str,
857    config: Arc<RwLock<ClientConfig>>,
858    gui: bool,
859    command_rx: &mut mpsc::Receiver<Message>,
860    result_tx: &mpsc::Sender<Message>,
861) -> Result<()> {
862    let (output_path, download_type, filename) =
863        handle_upgrade_download(version, gui, config, command_rx, result_tx).await?;
864
865    handle_upgrade_install(output_path, download_type, filename, command_rx, result_tx).await?;
866
867    Ok(())
868}