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    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    // Check for dpkg (Debian/Ubuntu systems)
302    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    // Check for rpm (Red Hat/SUSE systems)
312    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/// Generic function to run a script with given template and replacements
374#[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    // Use cache directory for updates instead of temp
385    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    // Apply all replacements to the template
405    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    // Always make script executable on Unix
415    #[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    // Execute the script
424    #[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) // CREATE_NO_WINDOW
430            .spawn()
431            .context("Failed to execute script")?;
432    }
433
434    #[cfg(target_os = "linux")]
435    {
436        if is_service {
437            // Use systemd one-shot service for service updates
438            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}", &timestamp)
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            // Use nohup for regular executable updates
467            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            // Use launchd one-shot job for service updates
486            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}", &timestamp)
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            // Use nohup for regular executable updates
511            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
530/// Run an upgrade script with logging support
531fn 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    // Extract config path for service mode
562    // Note: The actual flag used is --conf, not --config
563    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    // Always provide all replacements - unused ones will be ignored
576    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    // Get current command line arguments (skip the program name)
633    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    // Delete old update cache directory
761    let cache_dir = get_cache_dir("updates").unwrap();
762    std::fs::remove_dir_all(&cache_dir).ok();
763
764    // Download the upgrade
765    let cache_dir = get_cache_dir("updates").unwrap();
766
767    let download_type = if gui {
768        #[cfg(target_os = "linux")]
769        {
770            // Try to detect package type and use appropriate installer
771            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, // Fallback to CLI
775            }
776        }
777        #[cfg(not(target_os = "linux"))]
778        DownloadType::Gui
779    } else {
780        DownloadType::Cli
781    };
782
783    // Use base URL from config server field
784    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    // Extract file extension from download URL
789    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        // If the file extension is .gz, we assume it's a tar.gz file
797        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    // change the current directory to the cache directory
833    std::env::set_current_dir(cache_dir)
834        .context("Failed to change current directory to cache directory")?;
835
836    // Handle different package types
837    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 updates (tar.gz/zip extraction and replacement)
882            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}