From 722d57a65bbcf6f48f20123f113db93f1cd5283a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Tue, 26 Aug 2025 13:03:13 +0200 Subject: [PATCH 1/7] refactor!: change install location on Linux --- linkup-cli/install.py | 55 +++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/linkup-cli/install.py b/linkup-cli/install.py index 76674307..99c444f4 100755 --- a/linkup-cli/install.py +++ b/linkup-cli/install.py @@ -23,8 +23,6 @@ from pathlib import Path from typing import Any, Optional, Tuple, List -LINKUP_BIN_PATH = Path.home() / ".linkup" / "bin" - class Shell(Enum): bash = "bash" @@ -44,15 +42,15 @@ def from_str(value: str) -> Optional["Shell"]: else: return None - def add_to_profile_command(self) -> Optional[str]: + def add_to_profile_command(self, bin_path: Path) -> Optional[str]: if self == Shell.bash: return ( - f"echo 'export PATH=$PATH:{LINKUP_BIN_PATH}' >> {Path.home()}/.bashrc" + f"echo 'export PATH=$PATH:{bin_path}' >> {Path.home()}/.bashrc" ) elif self == Shell.zsh: - return f"echo 'export PATH=$PATH:{LINKUP_BIN_PATH}' >> {Path.home()}/.zshrc" + return f"echo 'export PATH=$PATH:{bin_path}' >> {Path.home()}/.zshrc" elif self == Shell.fish: - return f"echo 'set -gx PATH $PATH {LINKUP_BIN_PATH}' >> {Path.home()}/.config/fish/config.fish" + return f"echo 'set -gx PATH $PATH {bin_path}' >> {Path.home()}/.config/fish/config.fish" else: return None @@ -201,7 +199,11 @@ def tar_extract(tar: TarFile, path: str): def download_and_extract( - user_os: OS, user_arch: Arch, channel: Channel, release: GithubRelease + target_location: Path, + user_os: OS, + user_arch: Arch, + channel: Channel, + release: GithubRelease ) -> None: print(f"Latest release on {channel.name} channel: {release.tag_name}.") print(f"Looking for asset for {user_os.value}/{user_arch.value}...") @@ -235,25 +237,30 @@ def download_and_extract( with tarfile.open(local_tar_path, "r:gz") as tar: tar_extract(tar, "/tmp") - LINKUP_BIN_PATH.mkdir(parents=True, exist_ok=True) - linkup_bin_path = LINKUP_BIN_PATH / "linkup" - shutil.move("/tmp/linkup", linkup_bin_path) - os.chmod(linkup_bin_path, 0o755) + if user_os == OS.MacOS: + target_location.mkdir(parents=True, exist_ok=True) - if user_os == OS.Linux: + linkup_bin_path = target_location / "linkup" + shutil.move("/tmp/linkup", linkup_bin_path) + os.chmod(linkup_bin_path, 0o755) + elif user_os == OS.Linux: + linkup_bin_path = target_location / "linkup" + subprocess.run(["sudo", "mv", "/tmp/linkup", str(linkup_bin_path)], check=True) + subprocess.run(["sudo", "chmod", "755", str(linkup_bin_path)], check=True) subprocess.run( - ["sudo", "setcap", "cap_net_bind_service=+ep", f"{linkup_bin_path}"] + ["sudo", "setcap", "cap_net_bind_service=+ep", str(linkup_bin_path)], + check=True ) - print(f"Linkup installed at {LINKUP_BIN_PATH / 'linkup'}") + print(f"Linkup installed at {target_location / 'linkup'}") local_tar_path.unlink() -def setup_path() -> None: - if str(LINKUP_BIN_PATH) in os.environ.get("PATH", "").split(":"): +def setup_path(target_location: Path) -> None: + if str(target_location) in os.environ.get("PATH", "").split(":"): return - print(f"\nTo start using Linkup, add '{LINKUP_BIN_PATH}' to your PATH.") + print(f"\nTo start using Linkup, add '{target_location}' to your PATH.") shell = Shell.from_str(os.path.basename(os.environ.get("SHELL", ""))) if shell is None: @@ -262,7 +269,7 @@ def setup_path() -> None: print( f"Since you are using {shell.name}, you can run the following to add to your profile:" ) - print(f"\n {shell.add_to_profile_command()}") + print(f"\n {shell.add_to_profile_command(target_location)}") print("\nThen restart your shell.") @@ -293,9 +300,17 @@ def main() -> None: user_os, user_arch = detect_platform() release = get_release_data(context.channel) - download_and_extract(user_os, user_arch, context.channel, release) - setup_path() + if user_os == OS.MacOS: + target_location = Path.home() / ".linkup" / "bin" + elif user_os == OS.Linux: + target_location = Path("/") / "usr" / "local" / "bin" + else: + raise ValueError(f"Unsupported OS: {user_os}") + + download_and_extract(target_location, user_os, user_arch, context.channel, release) + + setup_path(target_location) print("Linkup installation complete! 🎉") From 2e7988400f2b01fccb93d44de54dc3e994616ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Tue, 26 Aug 2025 13:08:26 +0200 Subject: [PATCH 2/7] refactor: improve shared variables and cleanup --- linkup-cli/install.py | 57 +++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/linkup-cli/install.py b/linkup-cli/install.py index 99c444f4..68998366 100755 --- a/linkup-cli/install.py +++ b/linkup-cli/install.py @@ -226,34 +226,39 @@ def download_and_extract( print(f"Downloading: {download_url}") local_tar_path = Path("/tmp") / Path(download_url).name + local_temp_bin_path = Path("/tmp") / "linkup" + + try: + with ( + urllib.request.urlopen(download_url) as response, + open(local_tar_path, "wb") as out_file, + ): + shutil.copyfileobj(response, out_file) + + print(f"Decompressing {local_tar_path}") + with tarfile.open(local_tar_path, "r:gz") as tar: + tar_extract(tar, "/tmp") + + installation_bin_path = target_location / "linkup" + + if user_os == OS.MacOS: + target_location.mkdir(parents=True, exist_ok=True) + shutil.move(str(local_temp_bin_path), installation_bin_path) + os.chmod(installation_bin_path, 0o755) + elif user_os == OS.Linux: + subprocess.run(["sudo", "mv", str(local_temp_bin_path), str(installation_bin_path)], check=True) + subprocess.run(["sudo", "chmod", "755", str(installation_bin_path)], check=True) + subprocess.run( + ["sudo", "setcap", "cap_net_bind_service=+ep", str(installation_bin_path)], check=True + ) - with ( - urllib.request.urlopen(download_url) as response, - open(local_tar_path, "wb") as out_file, - ): - shutil.copyfileobj(response, out_file) - - print(f"Decompressing {local_tar_path}") - with tarfile.open(local_tar_path, "r:gz") as tar: - tar_extract(tar, "/tmp") - - if user_os == OS.MacOS: - target_location.mkdir(parents=True, exist_ok=True) - - linkup_bin_path = target_location / "linkup" - shutil.move("/tmp/linkup", linkup_bin_path) - os.chmod(linkup_bin_path, 0o755) - elif user_os == OS.Linux: - linkup_bin_path = target_location / "linkup" - subprocess.run(["sudo", "mv", "/tmp/linkup", str(linkup_bin_path)], check=True) - subprocess.run(["sudo", "chmod", "755", str(linkup_bin_path)], check=True) - subprocess.run( - ["sudo", "setcap", "cap_net_bind_service=+ep", str(linkup_bin_path)], - check=True - ) + print(f"Linkup installed at {installation_bin_path}") + finally: + if local_tar_path.exists(): + local_tar_path.unlink() - print(f"Linkup installed at {target_location / 'linkup'}") - local_tar_path.unlink() + if local_temp_bin_path.exists(): + local_temp_bin_path.unlink() def setup_path(target_location: Path) -> None: From 17e25ba6640f5456f95a4071fef7aca5a371faac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Tue, 26 Aug 2025 13:10:30 +0200 Subject: [PATCH 3/7] refactor: use tempfile module --- linkup-cli/install.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/linkup-cli/install.py b/linkup-cli/install.py index 68998366..dee6ab2f 100755 --- a/linkup-cli/install.py +++ b/linkup-cli/install.py @@ -17,6 +17,7 @@ import shutil import subprocess import tarfile +import tempfile import urllib.request from dataclasses import dataclass from enum import Enum @@ -225,8 +226,9 @@ def download_and_extract( sys.exit(1) print(f"Downloading: {download_url}") - local_tar_path = Path("/tmp") / Path(download_url).name - local_temp_bin_path = Path("/tmp") / "linkup" + temp_dir = Path(tempfile.gettempdir()) + local_tar_path = temp_dir / Path(download_url).name + local_temp_bin_path = temp_dir / "linkup" try: with ( @@ -237,7 +239,7 @@ def download_and_extract( print(f"Decompressing {local_tar_path}") with tarfile.open(local_tar_path, "r:gz") as tar: - tar_extract(tar, "/tmp") + tar_extract(tar, str(temp_dir)) installation_bin_path = target_location / "linkup" From 8107fb60a1ddb6de64cf7988dc757d25ce9d0681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Tue, 26 Aug 2025 13:12:28 +0200 Subject: [PATCH 4/7] feat: add timeout to http requests --- linkup-cli/install.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/linkup-cli/install.py b/linkup-cli/install.py index dee6ab2f..555272ae 100755 --- a/linkup-cli/install.py +++ b/linkup-cli/install.py @@ -171,7 +171,7 @@ def list_releases() -> List[GithubRelease]: }, ) - with urllib.request.urlopen(req) as response: + with urllib.request.urlopen(req, timeout=30) as response: return [GithubRelease.from_json(release) for release in json.load(response)] @@ -184,7 +184,7 @@ def get_latest_stable_release() -> GithubRelease: }, ) - with urllib.request.urlopen(req) as response: + with urllib.request.urlopen(req, timeout=30) as response: return GithubRelease.from_json(json.load(response)) @@ -232,7 +232,7 @@ def download_and_extract( try: with ( - urllib.request.urlopen(download_url) as response, + urllib.request.urlopen(download_url, timeout=60) as response, open(local_tar_path, "wb") as out_file, ): shutil.copyfileobj(response, out_file) From 46c95952ac85eda2b667fc543f86d1f39995a682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Tue, 26 Aug 2025 13:13:36 +0200 Subject: [PATCH 5/7] chore: remove unused function --- linkup-cli/src/main.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/linkup-cli/src/main.rs b/linkup-cli/src/main.rs index a7df5783..785d21d5 100644 --- a/linkup-cli/src/main.rs +++ b/linkup-cli/src/main.rs @@ -57,12 +57,6 @@ pub fn linkup_dir_path() -> PathBuf { path } -pub fn linkup_bin_dir_path() -> PathBuf { - let mut path = linkup_dir_path(); - path.push("bin"); - path -} - pub fn linkup_certs_dir_path() -> PathBuf { let mut path = linkup_dir_path(); path.push("certs"); From 84913dd3da7fb65d24dffb0cb57efb66be59fabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Tue, 26 Aug 2025 14:02:35 +0200 Subject: [PATCH 6/7] feat: use sudo to manage Linux installation --- linkup-cli/src/commands/uninstall.rs | 26 ++++++++++++++++- linkup-cli/src/commands/update.rs | 42 ++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/linkup-cli/src/commands/uninstall.rs b/linkup-cli/src/commands/uninstall.rs index c988cdb3..d4f1b6f1 100644 --- a/linkup-cli/src/commands/uninstall.rs +++ b/linkup-cli/src/commands/uninstall.rs @@ -5,6 +5,9 @@ use crate::{ local_config::LocalState, prompt, InstallationMethod, Result, }; +#[cfg(target_os = "linux")] +use crate::{is_sudo, sudo_su}; + #[derive(clap::Args)] pub struct Args {} @@ -55,7 +58,28 @@ pub async fn uninstall(_args: &Args, config_arg: &Option) -> Result<()> InstallationMethod::Manual => { log::debug!("Uninstalling linkup"); - fs::remove_file(&exe_path)?; + #[cfg(target_os = "linux")] + { + println!("Linkup needs sudo access to:"); + println!(" - Remove binary from {:?}", &exe_path); + + if !is_sudo() { + sudo_su()?; + } + + process::Command::new("sudo") + .args(["rm", "-f"]) + .arg(&exe_path) + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status()?; + } + + #[cfg(not(target_os = "linux"))] + { + fs::remove_file(&exe_path)?; + } } } diff --git a/linkup-cli/src/commands/update.rs b/linkup-cli/src/commands/update.rs index 47d1234e..6d47c3c8 100644 --- a/linkup-cli/src/commands/update.rs +++ b/linkup-cli/src/commands/update.rs @@ -3,6 +3,9 @@ use std::fs; use crate::{commands, current_version, linkup_exe_path, release, InstallationMethod, Result}; +#[cfg(target_os = "linux")] +use crate::{is_sudo, sudo_su}; + #[derive(clap::Args)] pub struct Args { /// Ignore the cached last version and check remote server again for the latest version. @@ -61,15 +64,36 @@ pub async fn update(args: &Args) -> Result<()> { let current_linkup_path = linkup_exe_path()?; let bkp_linkup_path = current_linkup_path.with_extension("bkp"); - fs::rename(¤t_linkup_path, &bkp_linkup_path) - .expect("failed to move the current exe into a backup"); - fs::rename(&new_linkup_path, ¤t_linkup_path) - .expect("failed to move the new exe as the current exe"); - #[cfg(target_os = "linux")] { println!("Linkup needs sudo access to:"); + println!(" - Update binary in {:?}", ¤t_linkup_path); println!(" - Add capability to bind to port 80/443"); + + if !is_sudo() { + sudo_su()?; + } + + std::process::Command::new("sudo") + .args(["mv"]) + .arg(¤t_linkup_path) + .arg(&bkp_linkup_path) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .expect("failed to move the current exe into a backup"); + + std::process::Command::new("sudo") + .args(["mv"]) + .arg(&new_linkup_path) + .arg(¤t_linkup_path) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .expect("failed to move the new exe as the current exe"); + std::process::Command::new("sudo") .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) @@ -82,6 +106,14 @@ pub async fn update(args: &Args) -> Result<()> { .spawn()?; } + #[cfg(not(target_os = "linux"))] + { + fs::rename(¤t_linkup_path, &bkp_linkup_path) + .expect("failed to move the current exe into a backup"); + fs::rename(&new_linkup_path, ¤t_linkup_path) + .expect("failed to move the new exe as the current exe"); + } + println!("Finished update!"); } None => { From 76181ea40307461113b8a20d18c176fe2a0a9864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Thu, 28 Aug 2025 09:26:43 +0200 Subject: [PATCH 7/7] fix: remove unused non-linux import --- linkup-cli/src/commands/update.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/linkup-cli/src/commands/update.rs b/linkup-cli/src/commands/update.rs index 6d47c3c8..d228c113 100644 --- a/linkup-cli/src/commands/update.rs +++ b/linkup-cli/src/commands/update.rs @@ -1,4 +1,5 @@ use anyhow::Context; +#[cfg(not(target_os = "linux"))] use std::fs; use crate::{commands, current_version, linkup_exe_path, release, InstallationMethod, Result};