mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-15 00:56:36 +03:00
827 lines
19 KiB
HTML
827 lines
19 KiB
HTML
#!/bin/sh
|
|
set -eu
|
|
|
|
PROGRAM="sylve-installer"
|
|
PROGRAM_VERSION="0.2.0"
|
|
|
|
# We should Flip this to 1 when pkg/port is ready and want this script
|
|
# to automatically use pkg instead of GitHub release binaries.
|
|
AUTO_SWITCH_TO_PKG=0
|
|
|
|
REPO_OWNER="AlchemillaHQ"
|
|
REPO_NAME="Sylve"
|
|
API_BASE="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}"
|
|
|
|
BIN_PATH="/usr/local/sbin/sylve"
|
|
RC_SCRIPT_PATH="/usr/local/etc/rc.d/sylve"
|
|
CONFIG_PATH="/usr/local/etc/sylve/config.json"
|
|
DEFAULT_DATA_PATH="/var/db/sylve"
|
|
|
|
YES_MODE=0
|
|
FORCE_MODE=0
|
|
DATA_PATH=""
|
|
SUBCOMMAND="install"
|
|
SUBCOMMAND_SET=0
|
|
|
|
ARCH=""
|
|
OS_RELEASE=""
|
|
RELEASE_TAG=""
|
|
ASSET_NAME=""
|
|
ASSET_URL=""
|
|
ASSET_DIGEST=""
|
|
GENERATED_ADMIN_PASSWORD=""
|
|
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
C_RESET="$(printf '\033[0m')"
|
|
C_RED="$(printf '\033[31m')"
|
|
C_GREEN="$(printf '\033[32m')"
|
|
C_YELLOW="$(printf '\033[33m')"
|
|
C_BLUE="$(printf '\033[34m')"
|
|
else
|
|
C_RESET=""
|
|
C_RED=""
|
|
C_GREEN=""
|
|
C_YELLOW=""
|
|
C_BLUE=""
|
|
fi
|
|
|
|
ascii_art() {
|
|
cat <<'EOF' | sed "s/@VERSION@/${PROGRAM_VERSION}/"
|
|
_____ __
|
|
/ ___/__ __/ / _____
|
|
\__ \/ / / / / | / / _ \
|
|
___/ / /_/ / /| |/ / __/
|
|
/____/\__, /_/ |___/\___/
|
|
/____/ Installer v@VERSION@
|
|
|
|
EOF
|
|
}
|
|
|
|
log_info() {
|
|
printf "%s[INFO]%s %s\n" "$C_BLUE" "$C_RESET" "$*"
|
|
}
|
|
|
|
log_warn() {
|
|
printf "%s[WARN]%s %s\n" "$C_YELLOW" "$C_RESET" "$*" >&2
|
|
}
|
|
|
|
log_error() {
|
|
printf "%s[ERROR]%s %s\n" "$C_RED" "$C_RESET" "$*" >&2
|
|
}
|
|
|
|
log_ok() {
|
|
printf "%s[OK]%s %s\n" "$C_GREEN" "$C_RESET" "$*"
|
|
}
|
|
|
|
die() {
|
|
log_error "$*"
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${PROGRAM} v${PROGRAM_VERSION}
|
|
|
|
Usage:
|
|
$PROGRAM [install|update] [flags]
|
|
|
|
Subcommands:
|
|
install Install Sylve and write/update config.json (default)
|
|
update Update Sylve binary
|
|
|
|
Flags:
|
|
--yes Non-interactive mode
|
|
--data-path <path> Data path for install config (default: ${DEFAULT_DATA_PATH})
|
|
--force Force update even if tag matches installed metadata
|
|
-h, --help Show this help message
|
|
|
|
Notes:
|
|
- AUTO_SWITCH_TO_PKG is currently set to ${AUTO_SWITCH_TO_PKG}.
|
|
- When AUTO_SWITCH_TO_PKG=1, install/update uses pkg install/upgrade sylve.
|
|
EOF
|
|
}
|
|
|
|
have_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
require_root() {
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
die "Root privileges are required. Re-run as root/doas/sudo."
|
|
fi
|
|
}
|
|
|
|
ask_input_default() {
|
|
prompt="$1"
|
|
default_value="$2"
|
|
|
|
if [ "$YES_MODE" -eq 1 ]; then
|
|
printf '%s\n' "$default_value"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -r /dev/tty ]; then
|
|
log_warn "No tty available; using default: ${default_value}"
|
|
printf '%s\n' "$default_value"
|
|
return 0
|
|
fi
|
|
|
|
printf "%s [%s]: " "$prompt" "$default_value" > /dev/tty
|
|
IFS= read -r answer < /dev/tty || answer=""
|
|
if [ -z "$answer" ]; then
|
|
printf '%s\n' "$default_value"
|
|
else
|
|
printf '%s\n' "$answer"
|
|
fi
|
|
}
|
|
|
|
ask_yes_no() {
|
|
question="$1"
|
|
default_answer="$2"
|
|
|
|
if [ "$YES_MODE" -eq 1 ]; then
|
|
if [ "$default_answer" = "yes" ]; then
|
|
log_info "$question -> yes (--yes default)"
|
|
return 0
|
|
fi
|
|
log_info "$question -> no (--yes default)"
|
|
return 1
|
|
fi
|
|
|
|
if [ ! -r /dev/tty ]; then
|
|
if [ "$default_answer" = "yes" ]; then
|
|
log_warn "$question -> yes (no tty, default)"
|
|
return 0
|
|
fi
|
|
log_warn "$question -> no (no tty, default)"
|
|
return 1
|
|
fi
|
|
|
|
if [ "$default_answer" = "yes" ]; then
|
|
suffix="[Y/n]"
|
|
else
|
|
suffix="[y/N]"
|
|
fi
|
|
|
|
while :; do
|
|
printf "%s %s " "$question" "$suffix" > /dev/tty
|
|
IFS= read -r reply < /dev/tty || reply=""
|
|
reply_lc=$(printf '%s' "$reply" | tr '[:upper:]' '[:lower:]')
|
|
case "$reply_lc" in
|
|
y|yes) return 0 ;;
|
|
n|no) return 1 ;;
|
|
"")
|
|
if [ "$default_answer" = "yes" ]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
;;
|
|
*) printf "Please answer yes or no.\n" > /dev/tty ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
generate_random_password() {
|
|
if have_cmd uuidgen; then
|
|
uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]'
|
|
return 0
|
|
fi
|
|
|
|
if have_cmd openssl; then
|
|
openssl rand -hex 16
|
|
return 0
|
|
fi
|
|
|
|
# Fallback (still non-static): timestamp + pid.
|
|
printf 'sylve-%s-%s' "$(date +%s)" "$$"
|
|
}
|
|
|
|
validate_platform_and_arch() {
|
|
os_name="$(uname -s 2>/dev/null || printf 'unknown')"
|
|
if [ "$os_name" != "FreeBSD" ]; then
|
|
die "Unsupported OS: $os_name. This script supports FreeBSD only."
|
|
fi
|
|
|
|
OS_RELEASE="$(uname -r 2>/dev/null || printf '')"
|
|
os_major="$(printf '%s' "$OS_RELEASE" | awk -F'[.-]' '{print $1}')"
|
|
os_minor="$(printf '%s' "$OS_RELEASE" | awk -F'[.-]' '{print $2}')"
|
|
|
|
case "$os_major" in
|
|
''|*[!0-9]*)
|
|
die "Unable to parse FreeBSD release from '$OS_RELEASE'"
|
|
;;
|
|
esac
|
|
case "$os_minor" in
|
|
''|*[!0-9]*)
|
|
os_minor=0
|
|
;;
|
|
esac
|
|
|
|
if [ "$os_major" -lt 15 ]; then
|
|
die "FreeBSD >= 15 is required (found: $OS_RELEASE)."
|
|
fi
|
|
|
|
machine_arch="$(uname -m 2>/dev/null || printf '')"
|
|
case "$machine_arch" in
|
|
amd64|x86_64) ARCH="amd64" ;;
|
|
arm64|aarch64) ARCH="arm64" ;;
|
|
*) die "Unsupported architecture: ${machine_arch}" ;;
|
|
esac
|
|
}
|
|
|
|
http_get() {
|
|
url="$1"
|
|
if have_cmd fetch; then
|
|
fetch -q -o - "$url"
|
|
return 0
|
|
fi
|
|
if have_cmd curl; then
|
|
curl -fsSL \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
"$url"
|
|
return 0
|
|
fi
|
|
die "Neither fetch nor curl is available."
|
|
}
|
|
|
|
download_file() {
|
|
url="$1"
|
|
dst="$2"
|
|
if have_cmd fetch; then
|
|
fetch -q -o "$dst" "$url"
|
|
return 0
|
|
fi
|
|
if have_cmd curl; then
|
|
curl -fsSL -o "$dst" "$url"
|
|
return 0
|
|
fi
|
|
die "Neither fetch nor curl is available."
|
|
}
|
|
|
|
sha256_file() {
|
|
target="$1"
|
|
if have_cmd sha256; then
|
|
sha256 -q "$target"
|
|
return 0
|
|
fi
|
|
if have_cmd shasum; then
|
|
shasum -a 256 "$target" | awk '{print $1}'
|
|
return 0
|
|
fi
|
|
if have_cmd openssl; then
|
|
openssl dgst -sha256 "$target" | awk '{print $2}'
|
|
return 0
|
|
fi
|
|
die "No SHA-256 tool found."
|
|
}
|
|
|
|
json_release_tag() {
|
|
printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1
|
|
}
|
|
|
|
json_error_message() {
|
|
printf '%s' "$1" | sed -n 's/.*"message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1
|
|
}
|
|
|
|
json_asset_metadata_with_jq() {
|
|
json_payload="$1"
|
|
asset_name="$2"
|
|
printf '%s\n' "$json_payload" | jq -r --arg n "$asset_name" '
|
|
.assets[]? | select(.name == $n) |
|
|
((.digest // "" | sub("^sha256:"; "")) + "\t" + (.browser_download_url // ""))
|
|
' | head -n 1
|
|
}
|
|
|
|
json_asset_metadata_plain() {
|
|
json_payload="$1"
|
|
asset_name="$2"
|
|
|
|
# Normalize compact JSON into line-oriented tokens so we can parse without jq.
|
|
normalized_json="$(printf '%s' "$json_payload" | sed 's/[{}]/\n&\n/g; s/,/\n/g')"
|
|
|
|
printf '%s\n' "$normalized_json" | awk -v asset="$asset_name" '
|
|
BEGIN { in_assets=0; hit=0; digest=""; url="" }
|
|
/"assets"[[:space:]]*:[[:space:]]*\[/ { in_assets=1; next }
|
|
in_assets == 1 {
|
|
if (hit == 0 && $0 ~ ("\"name\"[[:space:]]*:[[:space:]]*\"" asset "\"")) {
|
|
hit=1; digest=""; url=""; next
|
|
}
|
|
if (hit == 1) {
|
|
if ($0 ~ /"digest"[[:space:]]*:[[:space:]]*"sha256:/) {
|
|
line=$0
|
|
sub(/.*"digest"[[:space:]]*:[[:space:]]*"sha256:/, "", line)
|
|
sub(/".*/, "", line)
|
|
digest=line
|
|
}
|
|
if ($0 ~ /"browser_download_url"[[:space:]]*:[[:space:]]*"/) {
|
|
line=$0
|
|
sub(/.*"browser_download_url"[[:space:]]*:[[:space:]]*"/, "", line)
|
|
sub(/".*/, "", line)
|
|
url=line
|
|
}
|
|
if (digest != "" && url != "") { print digest "\t" url; exit }
|
|
if ($0 ~ /"name"[[:space:]]*:[[:space:]]*"sylve-/ && $0 !~ ("\"name\"[[:space:]]*:[[:space:]]*\"" asset "\"")) {
|
|
hit=0; digest=""; url=""
|
|
}
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
resolve_latest_release() {
|
|
release_url="${API_BASE}/releases/latest"
|
|
log_info "Resolving latest release metadata..."
|
|
|
|
release_json="$(http_get "$release_url" 2>/dev/null || true)"
|
|
[ -n "$release_json" ] || die "Failed to fetch release metadata."
|
|
|
|
RELEASE_TAG="$(json_release_tag "$release_json")"
|
|
if [ -z "$RELEASE_TAG" ]; then
|
|
api_message="$(json_error_message "$release_json")"
|
|
if [ -n "$api_message" ]; then
|
|
die "Failed to parse release tag. GitHub API message: ${api_message}"
|
|
fi
|
|
die "Failed to parse release tag from API response."
|
|
fi
|
|
|
|
ASSET_NAME="sylve-${ARCH}"
|
|
if have_cmd jq; then
|
|
asset_meta="$(json_asset_metadata_with_jq "$release_json" "$ASSET_NAME")"
|
|
else
|
|
asset_meta="$(json_asset_metadata_plain "$release_json" "$ASSET_NAME")"
|
|
fi
|
|
|
|
[ -n "$asset_meta" ] || die "Release asset '${ASSET_NAME}' not found in ${RELEASE_TAG}."
|
|
|
|
ASSET_DIGEST="$(printf '%s' "$asset_meta" | awk -F '\t' '{print $1}')"
|
|
ASSET_URL="$(printf '%s' "$asset_meta" | awk -F '\t' '{print $2}')"
|
|
[ -n "$ASSET_DIGEST" ] || die "Missing asset digest."
|
|
[ -n "$ASSET_URL" ] || die "Missing asset URL."
|
|
}
|
|
|
|
extract_version_tag() {
|
|
printf '%s\n' "$1" | sed -n 's/.*\(v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -n 1
|
|
}
|
|
|
|
detect_installed_binary_tag() {
|
|
if [ ! -x "$BIN_PATH" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Use -h for compatibility with older releases (for example v0.1.x).
|
|
out="$("$BIN_PATH" -h 2>&1 || true)"
|
|
tag="$(extract_version_tag "$out")"
|
|
if [ -n "$tag" ]; then
|
|
printf '%s\n' "$tag"
|
|
return 0
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
mktemp_file() {
|
|
if tempf="$(mktemp -t sylve-installer 2>/dev/null)"; then
|
|
printf '%s\n' "$tempf"
|
|
return 0
|
|
fi
|
|
mktemp "/tmp/sylve-installer.XXXXXX"
|
|
}
|
|
|
|
install_binary_from_release() {
|
|
resolve_latest_release
|
|
|
|
local_tag="$(detect_installed_binary_tag)"
|
|
|
|
if [ -n "$local_tag" ] && [ "$local_tag" = "$RELEASE_TAG" ] && [ "$FORCE_MODE" -eq 0 ] && [ -x "$BIN_PATH" ]; then
|
|
log_ok "Already at latest tag ${RELEASE_TAG}."
|
|
return 0
|
|
fi
|
|
|
|
log_info "Downloading ${ASSET_NAME} (${RELEASE_TAG})..."
|
|
tmp_bin="$(mktemp_file)"
|
|
trap 'rm -f "$tmp_bin"' EXIT INT TERM HUP
|
|
|
|
download_file "$ASSET_URL" "$tmp_bin" || die "Download failed."
|
|
|
|
actual_digest="$(sha256_file "$tmp_bin")"
|
|
if [ "$actual_digest" != "$ASSET_DIGEST" ]; then
|
|
die "Digest mismatch. expected=${ASSET_DIGEST} actual=${actual_digest}"
|
|
fi
|
|
log_ok "Digest verified."
|
|
|
|
mkdir -p "$(dirname "$BIN_PATH")"
|
|
if [ -f "$BIN_PATH" ]; then
|
|
backup_path="${BIN_PATH}.bak.$(date -u '+%Y%m%d%H%M%S')"
|
|
cp -p "$BIN_PATH" "$backup_path"
|
|
log_info "Backed up existing binary to ${backup_path}"
|
|
fi
|
|
|
|
install -m 755 "$tmp_bin" "$BIN_PATH"
|
|
log_ok "Installed ${BIN_PATH} (${RELEASE_TAG})"
|
|
|
|
rm -f "$tmp_bin"
|
|
trap - EXIT INT TERM HUP
|
|
}
|
|
|
|
pkg_install_or_update() {
|
|
if ! have_cmd pkg; then
|
|
die "'pkg' command not found."
|
|
fi
|
|
|
|
if pkg info -e sylve >/dev/null 2>&1; then
|
|
log_info "Updating pkg-installed sylve..."
|
|
ASSUME_ALWAYS_YES=yes pkg upgrade -y sylve
|
|
else
|
|
log_info "Installing sylve from pkg..."
|
|
ASSUME_ALWAYS_YES=yes pkg install -y sylve
|
|
fi
|
|
|
|
log_ok "pkg operation complete."
|
|
}
|
|
|
|
detect_samba_package() {
|
|
if ! have_cmd pkg; then
|
|
printf "samba423\n"
|
|
return 0
|
|
fi
|
|
|
|
installed_samba="$(pkg info 2>/dev/null | awk '/^samba4[0-9][0-9][0-9]*-/ { sub(/-.*/, "", $1); print $1; exit }')"
|
|
if [ -n "$installed_samba" ]; then
|
|
printf "%s\n" "$installed_samba"
|
|
return 0
|
|
fi
|
|
|
|
samba_pkg="$(pkg search -q '^samba4[0-9][0-9][0-9]*-' 2>/dev/null | sed 's/-.*//' | sort -r | head -n 1 || true)"
|
|
if [ -n "$samba_pkg" ]; then
|
|
printf "%s\n" "$samba_pkg"
|
|
return 0
|
|
fi
|
|
|
|
printf "samba423\n"
|
|
}
|
|
|
|
maybe_install_recommended_dependencies() {
|
|
if ! have_cmd pkg; then
|
|
log_warn "'pkg' command not found; skipping dependency installation."
|
|
return 0
|
|
fi
|
|
|
|
if ask_yes_no "Install recommended dependencies (libvirt, bhyve-firmware, swtpm, qemu-tools, samba4XX, dnsmasq)?" "yes"; then
|
|
samba_pkg="$(detect_samba_package)"
|
|
deps="libvirt bhyve-firmware swtpm qemu-tools dnsmasq ${samba_pkg}"
|
|
log_info "Installing dependencies: ${deps}"
|
|
ASSUME_ALWAYS_YES=yes pkg install -y $deps
|
|
log_ok "Recommended dependencies installed."
|
|
else
|
|
log_info "Skipped recommended dependency installation."
|
|
fi
|
|
}
|
|
|
|
write_default_config() {
|
|
mkdir -p "$(dirname "$CONFIG_PATH")"
|
|
if [ -z "$GENERATED_ADMIN_PASSWORD" ]; then
|
|
GENERATED_ADMIN_PASSWORD="$(generate_random_password)"
|
|
fi
|
|
cat > "$CONFIG_PATH" <<EOF
|
|
{
|
|
"environment": "production",
|
|
"proxyToVite": false,
|
|
"dataPath": "${DATA_PATH}",
|
|
"admin": {
|
|
"email": "admin@sylve.local",
|
|
"password": "${GENERATED_ADMIN_PASSWORD}"
|
|
},
|
|
"logLevel": 3,
|
|
"port": 8181,
|
|
"httpPort": 8182,
|
|
"raft": {
|
|
"reset": false
|
|
},
|
|
"btt": {
|
|
"rpc": {
|
|
"enabled": false,
|
|
"host": "127.0.0.1",
|
|
"port": 6890
|
|
},
|
|
"dht": {
|
|
"enabled": true,
|
|
"port": 7246
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
}
|
|
|
|
install_rc_script_if_missing() {
|
|
if [ -f "$RC_SCRIPT_PATH" ]; then
|
|
log_info "rc script already exists at ${RC_SCRIPT_PATH}; keeping it."
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$RC_SCRIPT_PATH")"
|
|
cat > "$RC_SCRIPT_PATH" <<EOF
|
|
#!/bin/sh
|
|
#
|
|
# PROVIDE: sylve
|
|
# REQUIRE: DAEMON NETWORKING
|
|
# KEYWORD: shutdown
|
|
#
|
|
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf to enable sylve:
|
|
#
|
|
# sylve_enable (bool): Set to "NO" by default.
|
|
# Set it to "YES" to enable sylve.
|
|
# sylve_user (user): Set to "root" by default.
|
|
# User to run sylve as.
|
|
# sylve_group (group): Set to "wheel" by default.
|
|
# Group to run sylve as.
|
|
# sylve_args (str): Set to "-config ${CONFIG_PATH}" by default.
|
|
# Extra flags passed to sylve.
|
|
|
|
. /etc/rc.subr
|
|
|
|
name=sylve
|
|
rcvar=sylve_enable
|
|
|
|
load_rc_config \$name
|
|
|
|
: \${sylve_enable:="NO"}
|
|
: \${sylve_user:="root"}
|
|
: \${sylve_group:="wheel"}
|
|
: \${sylve_args:="-config ${CONFIG_PATH}"}
|
|
|
|
export PATH="\${PATH}:/usr/local/bin:/usr/local/sbin"
|
|
|
|
pidfile="/var/run/\${name}.pid"
|
|
daemon_pidfile="/var/run/\${name}-daemon.pid"
|
|
procname="${BIN_PATH}"
|
|
command="/usr/sbin/daemon"
|
|
command_args="-f -c -R 5 -r -T \${name} -p \${pidfile} -P \${daemon_pidfile} \${procname} \${sylve_args}"
|
|
|
|
start_precmd=sylve_startprecmd
|
|
stop_postcmd=sylve_stoppostcmd
|
|
|
|
sylve_startprecmd()
|
|
{
|
|
if [ ! -e \${daemon_pidfile} ]; then
|
|
install -o \${sylve_user} -g \${sylve_group} /dev/null \${daemon_pidfile}
|
|
fi
|
|
if [ ! -e \${pidfile} ]; then
|
|
install -o \${sylve_user} -g \${sylve_group} /dev/null \${pidfile}
|
|
fi
|
|
}
|
|
|
|
sylve_stoppostcmd()
|
|
{
|
|
if [ -f "\${daemon_pidfile}" ]; then
|
|
pids=\$(pgrep -F \${daemon_pidfile} 2>&1)
|
|
_err=\$?
|
|
[ \${_err} -eq 0 ] && kill -9 \${pids}
|
|
fi
|
|
}
|
|
|
|
run_rc_command "\$1"
|
|
EOF
|
|
|
|
chmod 755 "$RC_SCRIPT_PATH"
|
|
log_ok "Installed rc script at ${RC_SCRIPT_PATH}"
|
|
}
|
|
|
|
set_config_datapath() {
|
|
[ -f "$CONFIG_PATH" ] || die "Config file not found: ${CONFIG_PATH}"
|
|
|
|
if have_cmd jq; then
|
|
tmp_json="$(mktemp_file)"
|
|
jq --arg p "$DATA_PATH" '.dataPath = $p' "$CONFIG_PATH" > "$tmp_json"
|
|
mv "$tmp_json" "$CONFIG_PATH"
|
|
return 0
|
|
fi
|
|
|
|
if grep -q '"dataPath"[[:space:]]*:' "$CONFIG_PATH"; then
|
|
escaped_path="$(printf '%s' "$DATA_PATH" | sed 's/[\/&]/\\&/g')"
|
|
tmp_json="$(mktemp_file)"
|
|
sed "s/\"dataPath\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"dataPath\": \"${escaped_path}\"/" "$CONFIG_PATH" > "$tmp_json"
|
|
mv "$tmp_json" "$CONFIG_PATH"
|
|
return 0
|
|
fi
|
|
|
|
tmp_json="$(mktemp_file)"
|
|
awk -v p="$DATA_PATH" '
|
|
BEGIN { inserted = 0 }
|
|
{
|
|
if (inserted == 0) {
|
|
idx = index($0, "{")
|
|
if (idx > 0) {
|
|
prefix = substr($0, 1, idx)
|
|
suffix = substr($0, idx + 1)
|
|
print prefix
|
|
print " \"dataPath\": \"" p "\","
|
|
if (length(suffix) > 0) print suffix
|
|
inserted = 1
|
|
next
|
|
}
|
|
}
|
|
print
|
|
}
|
|
' "$CONFIG_PATH" > "$tmp_json"
|
|
mv "$tmp_json" "$CONFIG_PATH"
|
|
}
|
|
|
|
configure_install_data_path() {
|
|
if [ -z "$DATA_PATH" ]; then
|
|
DATA_PATH="$(ask_input_default "Sylve data directory" "$DEFAULT_DATA_PATH")"
|
|
fi
|
|
|
|
[ -n "$DATA_PATH" ] || die "Data path cannot be empty."
|
|
|
|
if [ -f "$CONFIG_PATH" ]; then
|
|
log_info "Config exists at ${CONFIG_PATH}; updating only dataPath."
|
|
set_config_datapath
|
|
else
|
|
log_info "Creating config at ${CONFIG_PATH}."
|
|
write_default_config
|
|
fi
|
|
|
|
log_ok "Config ready with dataPath='${DATA_PATH}'."
|
|
}
|
|
|
|
maybe_enable_and_start_sylve() {
|
|
if [ ! -f "$RC_SCRIPT_PATH" ]; then
|
|
log_warn "rc script '${RC_SCRIPT_PATH}' not found; skipping service enable/start."
|
|
return 0
|
|
fi
|
|
|
|
if ! have_cmd sysrc; then
|
|
log_warn "'sysrc' not found; cannot enable sylve in rc.conf automatically."
|
|
return 0
|
|
fi
|
|
|
|
if ! have_cmd service; then
|
|
log_warn "'service' command not found; cannot start sylve automatically."
|
|
return 0
|
|
fi
|
|
|
|
if ask_yes_no "Enable Sylve service and start it now?" "yes"; then
|
|
sysrc sylve_enable=YES
|
|
if service sylve onestatus >/dev/null 2>&1; then
|
|
if service sylve restart; then
|
|
log_ok "Sylve service restarted."
|
|
else
|
|
log_warn "Failed to restart Sylve service. Please check service logs."
|
|
fi
|
|
else
|
|
if service sylve start; then
|
|
log_ok "Sylve service started."
|
|
else
|
|
log_warn "Failed to start Sylve service. Please check service logs."
|
|
fi
|
|
fi
|
|
else
|
|
log_info "Skipped enabling/starting Sylve service."
|
|
fi
|
|
}
|
|
|
|
get_https_port_from_config() {
|
|
if [ ! -f "$CONFIG_PATH" ]; then
|
|
printf "8181\n"
|
|
return 0
|
|
fi
|
|
|
|
port="$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$CONFIG_PATH" | head -n 1)"
|
|
if [ -n "$port" ]; then
|
|
printf "%s\n" "$port"
|
|
return 0
|
|
fi
|
|
|
|
printf "8181\n"
|
|
}
|
|
|
|
show_web_ui_urls() {
|
|
if ! have_cmd ifconfig; then
|
|
return 0
|
|
fi
|
|
|
|
https_port="$(get_https_port_from_config)"
|
|
if [ "$https_port" = "0" ]; then
|
|
return 0
|
|
fi
|
|
|
|
ip_list="$(ifconfig -a 2>/dev/null | awk '
|
|
/^[A-Za-z0-9].*:$/ { iface=$1; sub(/:$/, "", iface) }
|
|
$1 == "inet" {
|
|
ip=$2
|
|
if (ip !~ /^127\./) print ip
|
|
}
|
|
$1 == "inet6" {
|
|
ip=$2
|
|
sub(/%.*/, "", ip)
|
|
if (ip != "::1" && ip !~ /^fe80:/) print "[" ip "]"
|
|
}
|
|
' | sort -u)"
|
|
|
|
if [ -z "$ip_list" ]; then
|
|
return 0
|
|
fi
|
|
|
|
printf "\nAccess Web UI at:\n"
|
|
printf '%s\n' "$ip_list" | while IFS= read -r ip; do
|
|
[ -n "$ip" ] || continue
|
|
printf "https://%s:%s\n" "$ip" "$https_port"
|
|
done
|
|
printf "\n"
|
|
}
|
|
|
|
command_install() {
|
|
require_root
|
|
validate_platform_and_arch
|
|
|
|
if [ "$AUTO_SWITCH_TO_PKG" -eq 1 ]; then
|
|
log_info "AUTO_SWITCH_TO_PKG=1 -> using pkg install path."
|
|
pkg_install_or_update
|
|
else
|
|
install_binary_from_release
|
|
fi
|
|
|
|
configure_install_data_path
|
|
maybe_install_recommended_dependencies
|
|
install_rc_script_if_missing
|
|
maybe_enable_and_start_sylve
|
|
|
|
if [ -n "$GENERATED_ADMIN_PASSWORD" ]; then
|
|
printf "\n"
|
|
log_ok "Generated admin password has been written to ${CONFIG_PATH}"
|
|
printf "Admin username: admin\n"
|
|
printf "Admin password: %s\n" "$GENERATED_ADMIN_PASSWORD"
|
|
printf "\n"
|
|
fi
|
|
|
|
show_web_ui_urls
|
|
log_ok "Install completed."
|
|
}
|
|
|
|
command_update() {
|
|
require_root
|
|
validate_platform_and_arch
|
|
|
|
if [ "$AUTO_SWITCH_TO_PKG" -eq 1 ]; then
|
|
log_info "AUTO_SWITCH_TO_PKG=1 -> using pkg update path."
|
|
pkg_install_or_update
|
|
return 0
|
|
fi
|
|
|
|
install_binary_from_release
|
|
log_ok "Update completed."
|
|
}
|
|
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
install|update)
|
|
if [ "$SUBCOMMAND_SET" -eq 1 ]; then
|
|
die "Only one subcommand can be used."
|
|
fi
|
|
SUBCOMMAND="$1"
|
|
SUBCOMMAND_SET=1
|
|
shift
|
|
;;
|
|
--yes)
|
|
YES_MODE=1
|
|
shift
|
|
;;
|
|
--force)
|
|
FORCE_MODE=1
|
|
shift
|
|
;;
|
|
--data-path=*)
|
|
DATA_PATH="${1#*=}"
|
|
shift
|
|
;;
|
|
--data-path)
|
|
[ $# -ge 2 ] || die "--data-path requires a value."
|
|
DATA_PATH="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "Unknown argument: $1 (run with --help)"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
main() {
|
|
ascii_art
|
|
parse_args "$@"
|
|
|
|
case "$SUBCOMMAND" in
|
|
install) command_install ;;
|
|
update) command_update ;;
|
|
*) die "Unknown subcommand: ${SUBCOMMAND}" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|