use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use crate::{cli::UpgradeCommand, diagnostic_log, i18n, output};
const INSTALL_SH_URL: &str =
"https://raw.githubusercontent.com/EaveLuo/filelift/main/scripts/install.sh";
const INSTALL_PS1_URL: &str =
"https://raw.githubusercontent.com/EaveLuo/filelift/main/scripts/install.ps1";
const VERSION_ENV: &str = "FILELIFT_VERSION";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpgradePlan {
pub program: String,
pub args: Vec<String>,
pub version_env: Option<String>,
}
pub fn run(command: UpgradeCommand) -> Result<()> {
anstream::eprintln!("{}", output::info(&i18n::t("upgrade-starting")));
if running_from_cargo_bin() {
return run_cargo_upgrade(std::env::consts::OS, command.version);
}
let plan = build_plan(std::env::consts::OS, command.version)?;
let mut process = Command::new(&plan.program);
process.args(&plan.args);
if let Some(version) = &plan.version_env {
process.env(VERSION_ENV, version);
}
let status = process
.status()
.with_context(|| i18n::t_args("upgrade-spawn-failed", &[("program", &plan.program)]))?;
anyhow::ensure!(status.success(), i18n::t("upgrade-failed"));
diagnostic_log::record_command_result("upgrade", None, "success");
anstream::eprintln!("{}", output::success(&i18n::t("upgrade-succeeded")));
Ok(())
}
fn running_from_cargo_bin() -> bool {
let Ok(exe) = std::env::current_exe() else {
return false;
};
cargo_bin_dir()
.map(|dir| is_within(&exe, &dir))
.unwrap_or(false)
}
fn cargo_bin_dir() -> Option<PathBuf> {
if let Some(home) = std::env::var_os("CARGO_HOME") {
return Some(PathBuf::from(home).join("bin"));
}
dirs::home_dir().map(|home| home.join(".cargo").join("bin"))
}
fn is_within(path: &Path, dir: &Path) -> bool {
let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let dir = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
path.starts_with(&dir)
}
fn run_cargo_upgrade(target_os: &str, version: Option<String>) -> Result<()> {
let version = version.map(|version| version.trim().trim_start_matches('v').to_string());
if target_os == "windows" {
let mut command = "cargo install filelift --force".to_string();
if let Some(version) = &version {
command.push_str(&format!(" --version {version}"));
}
anstream::eprintln!(
"{}",
output::warning(&i18n::t_args(
"upgrade-cargo-manual",
&[("command", &command)]
))
);
return Ok(());
}
anstream::eprintln!("{}", output::info(&i18n::t("upgrade-via-cargo")));
let mut process = Command::new("cargo");
process.arg("install").arg("filelift").arg("--force");
if let Some(version) = &version {
process.arg("--version").arg(version);
}
let status = process
.status()
.with_context(|| i18n::t_args("upgrade-spawn-failed", &[("program", "cargo")]))?;
anyhow::ensure!(status.success(), i18n::t("upgrade-failed"));
diagnostic_log::record_command_result("upgrade", None, "success");
anstream::eprintln!("{}", output::success(&i18n::t("upgrade-succeeded")));
Ok(())
}
pub fn build_plan(target_os: &str, version: Option<String>) -> Result<UpgradePlan> {
let version_env = version.map(|version| version.trim().to_string());
match target_os {
"windows" => Ok(UpgradePlan {
program: "powershell".to_string(),
args: vec![
"-NoProfile".to_string(),
"-ExecutionPolicy".to_string(),
"Bypass".to_string(),
"-Command".to_string(),
format!("irm {INSTALL_PS1_URL} | iex"),
],
version_env,
}),
"macos" | "linux" => Ok(UpgradePlan {
program: "sh".to_string(),
args: vec![
"-c".to_string(),
format!("curl -fsSL {INSTALL_SH_URL} | sh"),
],
version_env,
}),
other => {
anyhow::bail!(i18n::t_args("upgrade-unsupported-os", &[("os", other)]))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn windows_plan_uses_powershell_and_install_script() {
let plan = build_plan("windows", None).unwrap();
assert_eq!(plan.program, "powershell");
assert!(plan.args.iter().any(|arg| arg.contains("install.ps1")));
assert!(plan.args.iter().any(|arg| arg.contains("iex")));
assert!(plan.version_env.is_none());
}
#[test]
fn unix_plan_uses_shell_and_install_script() {
let plan = build_plan("linux", None).unwrap();
assert_eq!(plan.program, "sh");
assert!(plan.args.iter().any(|arg| arg.contains("install.sh")));
assert_eq!(build_plan("macos", None).unwrap().program, "sh");
}
#[test]
fn version_is_passed_through_as_env() {
let plan = build_plan("linux", Some(" v0.3.0 ".to_string())).unwrap();
assert_eq!(plan.version_env.as_deref(), Some("v0.3.0"));
}
#[test]
fn unsupported_os_is_rejected() {
assert!(build_plan("freebsd", None).is_err());
}
#[test]
fn cargo_upgrade_on_windows_only_prints_guidance() {
assert!(run_cargo_upgrade("windows", Some("v1.2.3".to_string())).is_ok());
assert!(run_cargo_upgrade("windows", None).is_ok());
}
}