mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
Install script now removes index.html, QR codes, and download bundles from existing site directories before rebuild-all regenerates them. Preserves config.json, .onionshare-session, server.pid, and logs. Also auto-rebuilds on serve if index.html is missing. Co-Authored-By: Claude Opus 4.6 <[email protected]>
399 lines
14 KiB
Bash
Executable File
399 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO="PR0M3TH3AN/VoxVera"
|
|
REPO_URL="https://github.com/${REPO}"
|
|
BRANCH="main"
|
|
|
|
# --- 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; }
|
|
|
|
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 [ "$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
|
|
fi
|
|
|
|
msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify."
|
|
# 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"
|