Files
VoxVera/install.sh
T

456 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
REPO="PR0M3TH3AN/VoxVera"
REPO_URL="https://github.com/${REPO}"
BRANCH="main"
OS_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')"
# --- Network resilience settings ---
# Kill git operations that stall below 1 KB/s for 30 seconds
export GIT_HTTP_LOW_SPEED_LIMIT=1000
export GIT_HTTP_LOW_SPEED_TIME=30
# Disable partial clones — they are fragile on flaky connections
export GIT_CLONE_PROTECTION_ACTIVE=false
# Hard timeout (seconds) for any single network operation
NET_TIMEOUT=90
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Detect package manager and sudo
if command_exists sudo; then
SUDO="sudo"
elif [ "$(id -u)" -eq 0 ]; then
SUDO=""
else
echo "This script requires root privileges. Please install sudo or run as root." >&2
exit 1
fi
if command_exists apt-get; then
PM="apt"
elif command_exists dnf; then
PM="dnf"
elif command_exists yum; then
PM="yum"
elif command_exists pacman; then
PM="pacman"
elif command_exists brew; then
PM="brew"
elif command_exists apk; then
PM="apk"
else
echo "Unsupported system: could not detect package manager" >&2
exit 1
fi
msg() { echo -e "\e[32m$*\e[0m"; }
warn() { echo -e "\e[33m$*\e[0m" >&2; }
die() { echo -e "\e[31m$*\e[0m" >&2; exit 1; }
report_cli_status() {
command_exists voxvera || return 0
local platform_json doctor_json autostart_json
platform_json="$(voxvera platform-status --json 2>/dev/null || true)"
doctor_json="$(voxvera doctor --json 2>/dev/null || true)"
autostart_json="$(voxvera autostart status --json 2>/dev/null || true)"
if [ -n "$platform_json" ]; then
PLATFORM_JSON="$platform_json" python3 - <<'PY'
import json, os
data = json.loads(os.environ["PLATFORM_JSON"])
print(f"Platform tier: {data.get('tier', 'unknown')} ({data.get('label', 'unknown')})")
if data.get("hosting_model"):
print(f"Hosting model: {data['hosting_model']}")
PY
fi
if [ -n "$doctor_json" ]; then
DOCTOR_JSON="$doctor_json" python3 - <<'PY'
import json, os
data = json.loads(os.environ["DOCTOR_JSON"])
failed = [check["name"] for check in data.get("checks", []) if not check.get("ok")]
if failed:
print("Doctor checks needing attention: " + ", ".join(failed))
else:
print("Doctor checks: all passed")
PY
fi
if [ -n "$autostart_json" ]; then
AUTOSTART_JSON="$autostart_json" python3 - <<'PY'
import json, os
data = json.loads(os.environ["AUTOSTART_JSON"])
print("Autostart: " + data.get("message", "unknown"))
PY
fi
}
install_pkg() {
case "$PM" in
apt) $SUDO apt-get install -y "$@" ;;
dnf) $SUDO dnf install -y "$@" ;;
yum) $SUDO yum install -y "$@" ;;
pacman) $SUDO pacman -S --noconfirm "$@" ;;
brew) brew install "$@" ;;
apk) $SUDO apk add --no-cache "$@" ;;
esac
}
# --- Resilient download: tries curl then wget, with retries and timeout ---
download() {
local url="$1" dest="$2"
# Try curl first
if command_exists curl; then
local attempt
for attempt in 1 2 3; do
if timeout "$NET_TIMEOUT" curl -fsSL \
--retry 2 --retry-delay 3 --retry-connrefused \
--max-time "$NET_TIMEOUT" \
"$url" -o "$dest" 2>/dev/null; then
return 0
fi
warn "curl attempt $attempt failed for $(basename "$url")"
sleep 2
done
fi
# Try wget as fallback (different TLS stack — often works when curl doesn't)
if command_exists wget; then
local attempt
for attempt in 1 2 3; do
if timeout "$NET_TIMEOUT" wget -q --timeout=60 --tries=1 \
"$url" -O "$dest" 2>/dev/null; then
return 0
fi
warn "wget attempt $attempt failed for $(basename "$url")"
sleep 2
done
fi
return 1
}
# --- Resilient HTTP status check (for release tag redirect) ---
get_latest_tag() {
local tag=""
if command_exists curl; then
tag=$(timeout 30 curl -fsSLI -o /dev/null -w '%{url_effective}' \
"${REPO_URL}/releases/latest" 2>/dev/null | grep -oE '[^/]+$') || true
fi
if [ -z "$tag" ] && command_exists wget; then
tag=$(timeout 30 wget --max-redirect=5 -q -S --spider \
"${REPO_URL}/releases/latest" 2>&1 | grep -i 'Location:' | tail -1 | grep -oE '[^/]+$') || true
fi
echo "$tag"
}
# Ensure core dependencies are present
msg "Checking and updating system dependencies..."
if [[ "$OS_NAME" != linux* ]]; then
warn "This installer is optimized for Linux. Non-Linux paths are experimental and are not validated for reliable background hidden-service hosting."
fi
if [ "$PM" = "apt" ]; then
$SUDO apt-get update
SYSTEM_PKGS=(tor curl wget git python3-pip python3-venv)
[ -n "$(command -v pipx)" ] || SYSTEM_PKGS+=(pipx)
install_pkg "${SYSTEM_PKGS[@]}"
elif [ "$PM" = "brew" ]; then
brew install tor onionshare curl wget git
else
install_pkg tor curl git
# Best-effort install wget if not present
install_pkg wget 2>/dev/null || true
fi
# Onionshare-cli
msg "Ensuring onionshare-cli is installed and up-to-date..."
if command_exists pipx && (pipx list | grep -q onionshare || ! command_exists onionshare-cli); then
msg "Installing/Updating onionshare-cli via pipx..."
pipx install --force git+https://github.com/onionshare/onionshare.git#subdirectory=cli || warn "pipx onionshare-cli install/update failed"
elif ! command_exists onionshare-cli && ! command_exists onionshare; then
if ! install_pkg onionshare-cli; then
warn "Onionshare-cli not found in system repositories. Attempting pipx install fallback."
if ! command_exists pipx; then
install_pkg pipx
pipx ensurepath --force
export PATH="$HOME/.local/bin:$PATH"
fi
if command_exists pipx; then
msg "Installing onionshare-cli via pipx..."
pipx install --force git+https://github.com/onionshare/onionshare.git#subdirectory=cli || warn "pipx onionshare-cli install failed"
else
warn "Could not install pipx. Please install onionshare-cli manually."
fi
fi
else
msg "onionshare-cli found (system package), skipping pipx install. Run 'pipx install --force ...' if you want the latest git version."
fi
# --- Clean up stale state from previous installs (preserves ~/host tor sites) ---
msg "Cleaning up stale state from previous installs..."
# Stale user-local packages that may conflict with system onionshare-cli
for pydir in "$HOME"/.local/lib/python3.*/site-packages; do
[ -d "$pydir" ] || continue
for pkg in werkzeug flask flask_socketio onionshare_cli voxvera; do
if [ -d "$pydir/$pkg" ]; then
warn "Removing stale $pkg from $pydir"
rm -rf "$pydir/$pkg" "$pydir/${pkg}-"*.dist-info 2>/dev/null || true
fi
done
done
# Stale pip wheel cache for voxvera (forces clean rebuild)
for cache_dir in "$HOME"/.cache/pip/wheels; do
[ -d "$cache_dir" ] || continue
find "$cache_dir" -name 'voxvera-*.whl' -delete 2>/dev/null || true
done
# Leftover temp files from previous failed install attempts
rm -rf /tmp/voxvera-* 2>/dev/null || true
# Stale top-level config.json from older VoxVera versions (not inside ~/host)
[ -f "$HOME/config.json" ] && grep -q '"folder_name"' "$HOME/config.json" 2>/dev/null && {
warn "Removing stale ~/config.json (leftover from older VoxVera version)"
rm -f "$HOME/config.json"
}
# Stale top-level imports/ directory (older versions put it in ~/)
if [ -d "$HOME/imports" ] && ls "$HOME/imports"/*.json &>/dev/null; then
if head -1 "$HOME/imports"/*.json 2>/dev/null | grep -q '"folder_name"\|"headline"'; then
warn "Removing stale ~/imports/ (leftover from older VoxVera version)"
rm -rf "$HOME/imports"
fi
fi
# Clean generated files from existing site directories so rebuild-all
# regenerates them from the current template. Preserves:
# config.json — site content and settings
# .onionshare-session — Tor domain identity key
# server.pid — running process marker
# onionshare.log — current log (may be in use)
# Everything else is regenerated by build_assets().
if [ -d "$HOME/host" ]; then
for site_dir in "$HOME"/host/*/; do
[ -f "${site_dir}config.json" ] || continue
msg "Cleaning generated files in ${site_dir}..."
rm -f "${site_dir}index.html" 2>/dev/null || true
rm -f "${site_dir}qrcode-content.png" "${site_dir}qrcode-tear-offs.png" 2>/dev/null || true
rm -rf "${site_dir}download" 2>/dev/null || true
done
fi
# --- Helper: build pip flags for this distro ---
pip_flags() {
local flags="--user --upgrade"
if [ -f /etc/debian_version ]; then
local deb_ver
deb_ver=$(cut -d. -f1 < /etc/debian_version)
if [[ "$deb_ver" =~ ^[0-9]+$ ]] && [ "$deb_ver" -ge 12 ]; then
flags="$flags --break-system-packages"
fi
fi
echo "$flags"
}
# --- Helper: install from a local source directory ---
install_from_local() {
local src_dir="$1"
msg "Installing VoxVera from local source ($src_dir)..."
local flags
flags=$(pip_flags)
if pip install $flags "$src_dir"; then
msg "VoxVera installed from local source."
return 0
fi
return 1
}
# --- Helper: remove stale standalone binary so it doesn't shadow pipx/pip entrypoints ---
cleanup_stale_binary() {
local bin="$HOME/.local/bin/voxvera"
[ -f "$bin" ] || return 0
# Only remove actual compiled binaries (PyInstaller/native), not pip/pipx Python scripts
if file "$bin" 2>/dev/null | grep -qiE 'ELF|Mach-O'; then
warn "Removing stale standalone binary at $bin (would shadow pipx/pip entrypoint)"
rm -f "$bin"
fi
}
# ============================================================
# Strategy 5: Pre-built binary (fallback — can lag behind main)
# ============================================================
install_binary() {
msg "Strategy 5/5: Attempting to download pre-built binary..."
local latest
latest=$(get_latest_tag)
[ -n "$latest" ] || { warn "Could not determine latest release tag."; return 1; }
local install_dir="$HOME/.local/bin"
mkdir -p "$install_dir"
local dest="$install_dir/voxvera"
local os_name arch binary
os_name="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch=$(uname -m)
case "$os_name" in
linux*) binary="voxvera-linux" ;;
darwin*) binary="voxvera-macos" ;;
*) binary="voxvera-linux" ;;
esac
local url="${REPO_URL}/releases/download/${latest}/${binary}-${arch}"
msg "Downloading: $url"
if download "$url" "$dest"; then
chmod +x "$dest"
# Verify it's actually an executable, not an HTML error page
if file "$dest" | grep -qiE 'ELF|executable|Mach-O'; then
msg "VoxVera binary (${arch}) installed to $dest"
[[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && warn "Add \$HOME/.local/bin to your PATH."
return 0
fi
warn "Downloaded file is not a valid binary."
fi
[ -f "$dest" ] && rm -f "$dest"
warn "Binary download failed. Trying next method..."
return 1
}
# ============================================================
# Strategy 1: GitHub tarball — no git needed, always current with main
# ============================================================
install_tarball() {
msg "Strategy 1/5: Downloading source tarball (no git required)..."
local tmp_tar tmp_dir
tmp_tar=$(mktemp /tmp/voxvera-XXXXXX.tar.gz)
tmp_dir=$(mktemp -d /tmp/voxvera-src-XXXXXX)
if download "${REPO_URL}/archive/refs/heads/${BRANCH}.tar.gz" "$tmp_tar"; then
if tar -xzf "$tmp_tar" -C "$tmp_dir" --strip-components=1 2>/dev/null; then
if install_from_local "$tmp_dir"; then
cleanup_stale_binary
rm -rf "$tmp_tar" "$tmp_dir"
return 0
fi
else
warn "Tarball extraction failed."
fi
else
warn "Tarball download failed."
fi
rm -rf "$tmp_tar" "$tmp_dir"
warn "Tarball install failed. Trying next method..."
return 1
}
# ============================================================
# Strategy 2: Shallow git clone + local pip install
# ============================================================
install_shallow() {
msg "Strategy 2/5: Attempting shallow git clone (--depth 1)..."
local tmp_dir
tmp_dir=$(mktemp -d /tmp/voxvera-clone-XXXXXX)
if timeout "$NET_TIMEOUT" git clone --depth 1 --single-branch --branch "$BRANCH" \
"${REPO_URL}.git" "$tmp_dir" 2>&1; then
if install_from_local "$tmp_dir"; then
cleanup_stale_binary
rm -rf "$tmp_dir"
return 0
fi
else
warn "Shallow clone failed or timed out after ${NET_TIMEOUT}s."
fi
rm -rf "$tmp_dir"
warn "Shallow clone install failed. Trying next method..."
return 1
}
# ============================================================
# Strategy 3: pipx from git (isolated venv)
# ============================================================
install_pipx() {
command_exists pipx || return 1
msg "Strategy 3/5: Attempting pipx installation..."
if timeout 180 pipx install --force "voxvera@git+${REPO_URL}.git@${BRANCH}"; then
pipx inject voxvera setuptools 2>/dev/null || true
pipx ensurepath --force
cleanup_stale_binary
msg "\nVoxVera installed/updated successfully via pipx."
msg "IMPORTANT: Please restart your terminal or run 'source ~/.bashrc' to use 'voxvera'."
return 0
fi
warn "pipx install failed. Trying next method..."
return 1
}
# ============================================================
# Strategy 4: pip from git
# ============================================================
install_pip_git() {
msg "Strategy 4/5: Attempting pip install from git..."
local flags
flags=$(pip_flags)
if timeout 180 pip install $flags "voxvera@git+${REPO_URL}.git@${BRANCH}"; then
cleanup_stale_binary
msg "VoxVera installed via pip."
return 0
fi
warn "pip git install failed."
return 1
}
# ============================================================
# Run strategies in order — first success wins
# ============================================================
# Order rationale:
# 1. Tarball — always up-to-date with main, no git needed, single download
# 2. Shallow clone — small git transfer, also always current
# 3. pipx — git clone internally, but isolated venv
# 4. pip git — git clone internally, last resort
# 5. Binary — pre-built release; only fallback since it can lag behind main
for strategy in install_tarball install_shallow install_pipx install_pip_git install_binary; do
if $strategy; then
# Ensure ~/.local/bin is on PATH
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
export PATH="$HOME/.local/bin:$PATH"
fi
# Clear bash's cached command path so the new binary is found immediately
hash -r 2>/dev/null || true
# Rebuild all existing sites so they pick up template/CSS changes
if command_exists voxvera; then
msg "Rebuilding existing sites with updated template..."
voxvera rebuild-all 2>/dev/null || true
if [[ "$OS_NAME" == linux* ]]; then
msg "Installing Linux autostart recovery timer..."
voxvera autostart 2>/dev/null || warn "Could not install the VoxVera autostart timer automatically. Run 'voxvera autostart' manually."
else
warn "Autostart was not configured automatically because this platform is experimental."
fi
msg "Inspecting installed runtime posture..."
report_cli_status
fi
msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify."
if [[ "$OS_NAME" == linux* ]]; then
msg "Linux systemd user autostart should now retry hidden-service startup every few minutes."
msg "Inspect with: voxvera autostart status"
msg "Remove with: voxvera autostart uninstall"
msg "Validate with: bash scripts/linux-recovery-check.sh"
fi
# Detect if the parent shell will still have a stale hash
msg "If 'voxvera' is not found, run: hash -r (or open a new terminal)"
exit 0
fi
done
die "All installation methods failed. Your network may be blocking or corrupting HTTPS traffic to GitHub.\nTry downloading manually from a working machine:\n ${REPO_URL}/archive/refs/heads/${BRANCH}.tar.gz\nThen copy to this machine and run:\n tar xzf VoxVera-main.tar.gz && pip install --user ./VoxVera-main"