Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 59 additions & 37 deletions linkup-cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
import shutil
import subprocess
import tarfile
import tempfile
import urllib.request
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Optional, Tuple, List

LINKUP_BIN_PATH = Path.home() / ".linkup" / "bin"


class Shell(Enum):
bash = "bash"
Expand All @@ -44,15 +43,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

Expand Down Expand Up @@ -172,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)]


Expand All @@ -185,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))


Expand All @@ -201,7 +200,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}...")
Expand All @@ -223,37 +226,48 @@ def download_and_extract(
sys.exit(1)

print(f"Downloading: {download_url}")
local_tar_path = Path("/tmp") / Path(download_url).name

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")

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)
temp_dir = Path(tempfile.gettempdir())
local_tar_path = temp_dir / Path(download_url).name
local_temp_bin_path = temp_dir / "linkup"

try:
with (
urllib.request.urlopen(download_url, timeout=60) 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, str(temp_dir))

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
)

if user_os == OS.Linux:
subprocess.run(
["sudo", "setcap", "cap_net_bind_service=+ep", f"{linkup_bin_path}"]
)
print(f"Linkup installed at {installation_bin_path}")
finally:
if local_tar_path.exists():
local_tar_path.unlink()

print(f"Linkup installed at {LINKUP_BIN_PATH / 'linkup'}")
local_tar_path.unlink()
if local_temp_bin_path.exists():
local_temp_bin_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:
Expand All @@ -262,7 +276,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.")


Expand Down Expand Up @@ -293,9 +307,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! 🎉")

Expand Down
26 changes: 25 additions & 1 deletion linkup-cli/src/commands/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down Expand Up @@ -55,7 +58,28 @@ pub async fn uninstall(_args: &Args, config_arg: &Option<String>) -> 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)?;
}
}
}

Expand Down
43 changes: 38 additions & 5 deletions linkup-cli/src/commands/update.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use anyhow::Context;
#[cfg(not(target_os = "linux"))]
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.
Expand Down Expand Up @@ -61,15 +65,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(&current_linkup_path, &bkp_linkup_path)
.expect("failed to move the current exe into a backup");
fs::rename(&new_linkup_path, &current_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 {:?}", &current_linkup_path);
println!(" - Add capability to bind to port 80/443");

if !is_sudo() {
sudo_su()?;
}

std::process::Command::new("sudo")
.args(["mv"])
.arg(&current_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(&current_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())
Expand All @@ -82,6 +107,14 @@ pub async fn update(args: &Args) -> Result<()> {
.spawn()?;
}

#[cfg(not(target_os = "linux"))]
{
fs::rename(&current_linkup_path, &bkp_linkup_path)
.expect("failed to move the current exe into a backup");
fs::rename(&new_linkup_path, &current_linkup_path)
.expect("failed to move the new exe as the current exe");
}

println!("Finished update!");
}
None => {
Expand Down
6 changes: 0 additions & 6 deletions linkup-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading