Skip to content
Merged
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
250 changes: 238 additions & 12 deletions scripts/linux/install.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,122 @@
#!/usr/bin/env bash

# Exit codes
EXIT_GENERAL=1
EXIT_DEPENDENCY=2
EXIT_VERSION_FETCH=3
EXIT_DOWNLOAD=4
EXIT_UNSUPPORTED_ARCH=5

# Retry configuration
MAX_RETRIES=5
INITIAL_DELAY=1
MAX_DELAY=60
DOWNLOAD_TIMEOUT=60

# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (use sudo)"
exit 1
exit $EXIT_GENERAL
fi

check_dependencies() {
# Check for curl
if ! command -v curl &> /dev/null; then
echo "ERROR: curl is required but not installed."
echo "Please install curl and try again."
exit $EXIT_DEPENDENCY
fi

# Check for gzip validation tool (in priority order)
if command -v xxd &> /dev/null; then
GZIP_VALIDATOR="xxd"
elif command -v od &> /dev/null; then
GZIP_VALIDATOR="od"
elif command -v file &> /dev/null; then
GZIP_VALIDATOR="file"
else
echo "ERROR: No gzip validation tool found."
echo "Please install one of: xxd, od, or file"
exit $EXIT_DEPENDENCY
fi
}

create_temp_dir() {
TEMP_DIR=$(mktemp -d)
if [ ! -d "$TEMP_DIR" ]; then
echo "ERROR: Failed to create temporary directory."
exit $EXIT_GENERAL
fi
echo "Using temporary directory: $TEMP_DIR"
}

validate_gzip() {
local file="$1"

if [ ! -f "$file" ]; then
return 1
fi

case $GZIP_VALIDATOR in
xxd)
local magic=$(xxd -p -l 2 "$file" 2>/dev/null)
[ "$magic" = "1f8b" ]
;;
od)
local magic=$(od -A n -t x1 -N 2 "$file" 2>/dev/null | tr -d ' \n')
[ "$magic" = "1f8b" ]
;;
file)
file "$file" 2>/dev/null | grep -q gzip
;;
*)
return 1
;;
esac
}

calculate_backoff() {
local attempt=$1
local delay=$((INITIAL_DELAY << (attempt - 1)))
if [ "$delay" -gt "$MAX_DELAY" ]; then
delay=$MAX_DELAY
fi
# Add jitter (0-1 second)
local jitter
jitter=$(awk 'BEGIN{srand(); printf "%.2f", rand()}')
awk "BEGIN{printf \"%.2f\", $delay + $jitter}"
}

show_download_error() {
local url="$1"
local http_status="$2"
local file="$3"
local file_size=0
local file_content=""

if [ -f "$file" ]; then
file_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown")
file_content=$(head -c 100 "$file" 2>/dev/null | tr -cd '[:print:]')
fi

echo ""
echo "ERROR: Failed to download hostlink after $MAX_RETRIES attempts."
echo ""
echo "Last attempt details:"
echo " - HTTP Status: $http_status"
echo " - File size received: $file_size bytes"
if [ -n "$file_content" ]; then
echo " - File content (first 100 chars): \"$file_content\""
fi
echo " - Expected: Valid gzip archive (>1MB typically)"
echo ""
echo "Manual download URL:"
echo " $url"
echo ""
echo "You can download this file manually and extract it to /usr/bin/hostlink"
echo "Downloaded file location: $file"
}

# Default values
TOKEN_ID="default-token-id"
TOKEN_KEY="default-token-key"
Expand Down Expand Up @@ -61,8 +172,63 @@ uninstall_existing() {
}

latest_version() {
local version=$(curl -s https://api.github.com/repos/selfhost-dev/hostlink/releases/latest | grep tag_name | cut -d'"' -f4)
echo $version
local api_url="https://api.github.com/repos/selfhost-dev/hostlink/releases/latest"
local attempt=0
local version=""
local http_status=""
local temp_file="$TEMP_DIR/version_response.json"

echo "Fetching latest version..." >&2

while [ "$attempt" -lt "$MAX_RETRIES" ]; do
attempt=$((attempt + 1))

# Fetch with timeout and capture HTTP status
http_status=$(curl -sS --max-time "$DOWNLOAD_TIMEOUT" -w '%{http_code}' -o "$temp_file" "$api_url" 2>/dev/null)

# Check for 404 - fail immediately
if [ "$http_status" = "404" ]; then
echo "ERROR: Could not find release information (HTTP 404)." >&2
echo "The GitHub API endpoint may have changed or the repository may not exist." >&2
exit $EXIT_VERSION_FETCH
fi

# Check for success (2xx status)
if [[ "$http_status" =~ ^2[0-9][0-9]$ ]]; then
version=$(grep tag_name "$temp_file" 2>/dev/null | cut -d'"' -f4)

if [ -n "$version" ]; then
echo "Latest version: $version" >&2
rm -f "$temp_file"
echo "$version"
return 0
fi
fi

# If we get here, we need to retry (unless it's the last attempt)
if [ $attempt -lt $MAX_RETRIES ]; then
local delay
delay=$(calculate_backoff $attempt)
echo "Version fetch failed (HTTP $http_status). Retry $attempt/$MAX_RETRIES in ${delay}s..." >&2
sleep "$delay"
fi
done

# All retries exhausted
echo "" >&2
echo "ERROR: Failed to fetch latest version after $MAX_RETRIES attempts." >&2
echo "Last HTTP status: $http_status" >&2
if [ -f "$temp_file" ]; then
local content
content=$(head -c 100 "$temp_file" 2>/dev/null | tr -cd '[:print:]')
if [ -n "$content" ]; then
echo "Response content: \"$content\"" >&2
fi
fi
echo "" >&2
echo "Please check your internet connection and try again." >&2
rm -f "$temp_file"
exit $EXIT_VERSION_FETCH
}

detect_arch() {
Expand All @@ -75,28 +241,88 @@ detect_arch() {
echo "arm64"
;;
*)
echo "Unsupported architecture: $arch" >&2
exit 1
echo "ERROR: Unsupported architecture: $arch" >&2
exit $EXIT_UNSUPPORTED_ARCH
;;
esac
}

VERSION=$(latest_version)
# Run dependency checks first
check_dependencies
create_temp_dir

ARCH=$(detect_arch)

# latest_version outputs messages to stderr, version to stdout
VERSION=$(latest_version)
if [ -z "$VERSION" ]; then
echo "ERROR: Failed to determine version."
exit $EXIT_VERSION_FETCH
fi

HOSTLINK_TAR=hostlink_$VERSION.tar.gz

download_tar() {
curl -L -o $HOSTLINK_TAR \
https://github.com/selfhost-dev/hostlink/releases/download/${VERSION}/hostlink_Linux_${ARCH}.tar.gz
local download_url="https://github.com/selfhost-dev/hostlink/releases/download/${VERSION}/hostlink_Linux_${ARCH}.tar.gz"
local tar_file="$TEMP_DIR/$HOSTLINK_TAR"
local attempt=0
local http_status=""

echo "Downloading hostlink $VERSION..."

while [ "$attempt" -lt "$MAX_RETRIES" ]; do
attempt=$((attempt + 1))

# Download with timeout and capture HTTP status
http_status=$(curl -L -sS --max-time "$DOWNLOAD_TIMEOUT" -w '%{http_code}' -o "$tar_file" "$download_url" 2>/dev/null)

# Check for 404 - fail immediately
if [ "$http_status" = "404" ]; then
echo ""
echo "ERROR: Release not found (HTTP 404)."
echo "Version $VERSION may not exist or the release assets may not be available."
echo ""
echo "Manual download URL:"
echo " $download_url"
exit $EXIT_DOWNLOAD
fi

# Check for success (2xx status) and validate gzip
if [[ "$http_status" =~ ^2[0-9][0-9]$ ]]; then
if validate_gzip "$tar_file"; then
echo "Download successful."
return 0
else
echo "Download completed but file is not valid gzip (possibly corrupted)."
fi
fi

# If we get here, we need to retry (unless it's the last attempt)
if [ "$attempt" -lt "$MAX_RETRIES" ]; then
local delay
delay=$(calculate_backoff "$attempt")
if [[ "$http_status" =~ ^2[0-9][0-9]$ ]]; then
echo "Validation failed. Retry $attempt/$MAX_RETRIES in ${delay}s..."
else
echo "Download failed (HTTP $http_status). Retry $attempt/$MAX_RETRIES in ${delay}s..."
fi
sleep "$delay"
fi
done

# All retries exhausted
show_download_error "$download_url" "$http_status" "$tar_file"
exit $EXIT_DOWNLOAD
}

extract_tar() {
tar -xvf $HOSTLINK_TAR
echo "Extracting archive..."
tar -xvf "$TEMP_DIR/$HOSTLINK_TAR" -C "$TEMP_DIR"
}

move_bin() {
echo "Moving binary to /usr/bin, password prompt might be required."
sudo mv ./hostlink /usr/bin/hostlink
echo "Moving binary to /usr/bin..."
sudo mv "$TEMP_DIR/hostlink" /usr/bin/hostlink
}

create_directories() {
Expand All @@ -123,7 +349,7 @@ EOF

install_service() {
echo "Installing systemd service..."
sudo cp ./scripts/hostlink.service /etc/systemd/system/
sudo cp "$TEMP_DIR/scripts/hostlink.service" /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable hostlink
sudo systemctl start hostlink
Expand Down