- Replace fragile head -c 100 | grep content check with curl -w
'%{http_code}' for accurate HTTP status detection
- The old check incorrectly rejected legitimate HTML template files
(base.html starts with <!DOCTYPE html>)
- wget fallback uses its exit code instead (no content scanning)
260 lines
8.8 KiB
Bash
Executable file
260 lines
8.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# inBOXER Install Script
|
|
# ======================
|
|
# Downloads the latest inBOXER release from the git repository and
|
|
# deploys it to /opt/inboxer as a systemd service.
|
|
# Supports Debian (apt) and RHEL (yum/dnf) families.
|
|
#
|
|
# Usage:
|
|
# curl -sSL https://git.lohmar.co.uk/cclohmar/inboxer/raw/branch/main/install.sh | sudo bash
|
|
#
|
|
set -euo pipefail
|
|
|
|
# --- Configuration -----------------------------------------------------------
|
|
INSTALL_DIR="/opt/inboxer"
|
|
BIN_DIR="${INSTALL_DIR}/bin"
|
|
DATA_DIR="${INSTALL_DIR}/data"
|
|
LOGS_DIR="${INSTALL_DIR}/logs"
|
|
|
|
SERVICE_USER="inboxer"
|
|
SERVICE_GROUP="inboxer"
|
|
SERVICE_NAME="inboxer"
|
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
|
|
|
# Repository raw content base URL
|
|
REPO_BASE="https://git.lohmar.co.uk/cclohmar/inboxer/raw/branch/main"
|
|
|
|
# --- Terminal colours --------------------------------------------------------
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
|
|
# --- Pre-flight checks -------------------------------------------------------
|
|
if [[ $EUID -ne 0 ]]; then
|
|
error "This script must be run as root (use sudo)."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v systemctl &>/dev/null; then
|
|
error "systemd is required but not found on this system."
|
|
exit 1
|
|
fi
|
|
|
|
# Prefer curl; fall back to wget
|
|
DOWNLOADER=""
|
|
if command -v curl &>/dev/null; then
|
|
DOWNLOADER="curl -sSL"
|
|
elif command -v wget &>/dev/null; then
|
|
DOWNLOADER="wget -q -O"
|
|
else
|
|
error "Neither curl nor wget found. Install one of them and re-run."
|
|
exit 1
|
|
fi
|
|
|
|
# --- OS Detection (informational) -------------------------------------------
|
|
if [[ -f /etc/os-release ]]; then
|
|
# shellcheck source=/dev/null
|
|
. /etc/os-release
|
|
info "Detected OS: ${NAME} ${VERSION_ID}"
|
|
fi
|
|
|
|
# --- Create system user & group ----------------------------------------------
|
|
info "Creating system user '${SERVICE_USER}'..."
|
|
|
|
if getent group "${SERVICE_GROUP}" &>/dev/null; then
|
|
info "Group '${SERVICE_GROUP}' already exists."
|
|
else
|
|
groupadd --system "${SERVICE_GROUP}"
|
|
fi
|
|
|
|
if getent passwd "${SERVICE_USER}" &>/dev/null; then
|
|
info "User '${SERVICE_USER}' already exists."
|
|
else
|
|
useradd --system \
|
|
--no-create-home \
|
|
--gid "${SERVICE_GROUP}" \
|
|
--shell /sbin/nologin \
|
|
--comment "inBOXER Service User" \
|
|
"${SERVICE_USER}"
|
|
fi
|
|
|
|
# --- Create directory structure ----------------------------------------------
|
|
WEB_DIR="${INSTALL_DIR}/web"
|
|
TEMPLATES_DIR="${WEB_DIR}/templates"
|
|
STATIC_DIR="${WEB_DIR}/static"
|
|
|
|
info "Creating directories under ${INSTALL_DIR}..."
|
|
mkdir -p "${BIN_DIR}" "${DATA_DIR}" "${LOGS_DIR}" "${TEMPLATES_DIR}" "${STATIC_DIR}"
|
|
|
|
# --- Download helper ---------------------------------------------------------
|
|
download() {
|
|
local src_url="$1"
|
|
local dest_path="$2"
|
|
local mode="$3"
|
|
|
|
local tmpfile
|
|
tmpfile="$(mktemp)"
|
|
|
|
local http_code
|
|
if [[ "${DOWNLOADER}" == curl* ]]; then
|
|
http_code=$(curl -sSL -o "${tmpfile}" -w "%{http_code}" "${src_url}")
|
|
else
|
|
wget -q -O "${tmpfile}" "${src_url}" || {
|
|
rm -f "${tmpfile}"
|
|
error "Failed to download from ${src_url}"
|
|
return 1
|
|
}
|
|
# wget doesn't expose the HTTP code; assume success if no error
|
|
http_code="200"
|
|
fi
|
|
|
|
# Check HTTP status code (curl: exact; wget: best-effort via content)
|
|
if [[ "${DOWNLOADER}" == curl* ]] && [[ "${http_code}" != "200" ]]; then
|
|
rm -f "${tmpfile}"
|
|
error "Server returned HTTP ${http_code} for ${src_url}"
|
|
return 1
|
|
fi
|
|
|
|
# Check that the downloaded content is non-empty
|
|
if [[ ! -s "${tmpfile}" ]]; then
|
|
rm -f "${tmpfile}"
|
|
error "Downloaded empty file from ${src_url}"
|
|
return 1
|
|
fi
|
|
|
|
install -m "${mode}" "${tmpfile}" "${dest_path}"
|
|
rm -f "${tmpfile}"
|
|
info " ${src_url##*/} -> ${dest_path}"
|
|
}
|
|
|
|
# --- Download release files --------------------------------------------------
|
|
info "Downloading inBOXER release from ${REPO_BASE}/bin/ ..."
|
|
|
|
download "${REPO_BASE}/bin/inboxer" "${BIN_DIR}/inboxer" 755
|
|
download "${REPO_BASE}/bin/config.yaml" "${BIN_DIR}/config.yaml" 644
|
|
download "${REPO_BASE}/bin/prompt.txt" "${BIN_DIR}/prompt.txt" 644
|
|
|
|
# Web templates and static assets
|
|
info "Downloading web templates and static assets..."
|
|
download "${REPO_BASE}/web/templates/base.html" "${TEMPLATES_DIR}/base.html" 644
|
|
download "${REPO_BASE}/web/templates/login.html" "${TEMPLATES_DIR}/login.html" 644
|
|
download "${REPO_BASE}/web/templates/verify.html" "${TEMPLATES_DIR}/verify.html" 644
|
|
download "${REPO_BASE}/web/templates/dashboard.html" "${TEMPLATES_DIR}/dashboard.html" 644
|
|
download "${REPO_BASE}/web/templates/settings.html" "${TEMPLATES_DIR}/settings.html" 644
|
|
download "${REPO_BASE}/web/static/style.css" "${STATIC_DIR}/style.css" 644
|
|
|
|
# Adjust config paths for /opt/inboxer deployment
|
|
info "Adjusting configuration paths for deployment..."
|
|
sed -i 's|path: "bin/db.sqlite"|path: "'"${DATA_DIR}"'/db.sqlite"|' "${BIN_DIR}/config.yaml"
|
|
sed -i 's|file: "bin/inboxer.log"|file: "'"${LOGS_DIR}"'/inboxer.log"|' "${BIN_DIR}/config.yaml"
|
|
|
|
# --- Create systemd service unit ---------------------------------------------
|
|
info "Creating systemd service unit at ${SERVICE_FILE} ..."
|
|
cat > "${SERVICE_FILE}" << UNITEOF
|
|
[Unit]
|
|
Description=inBOXER - AI-Powered Email Classifier
|
|
Documentation=https://git.lohmar.co.uk/cclohmar/inboxer
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${SERVICE_USER}
|
|
Group=${SERVICE_GROUP}
|
|
WorkingDirectory=${INSTALL_DIR}
|
|
ExecStart=${BIN_DIR}/inboxer
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
# Logging
|
|
StandardOutput=append:${LOGS_DIR}/stdout.log
|
|
StandardError=append:${LOGS_DIR}/stderr.log
|
|
|
|
# Security hardening
|
|
NoNewPrivileges=true
|
|
ProtectSystem=full
|
|
ProtectHome=true
|
|
PrivateTmp=true
|
|
|
|
# Data and logs directories need write access (overrides ProtectSystem=full)
|
|
ReadWritePaths=${DATA_DIR} ${LOGS_DIR}
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNITEOF
|
|
|
|
# --- Set file permissions ----------------------------------------------------
|
|
info "Setting file ownership and permissions..."
|
|
chown -R "${SERVICE_USER}:${SERVICE_GROUP}" "${INSTALL_DIR}"
|
|
|
|
chmod 755 "${BIN_DIR}"
|
|
chmod 750 "${DATA_DIR}"
|
|
chmod 750 "${LOGS_DIR}"
|
|
chmod 640 "${BIN_DIR}/config.yaml"
|
|
|
|
# --- Register & start service -----------------------------------------------
|
|
info "Reloading systemd daemon..."
|
|
systemctl daemon-reload
|
|
|
|
info "Enabling ${SERVICE_NAME} service (starts on boot)..."
|
|
systemctl enable "${SERVICE_NAME}"
|
|
|
|
info "Starting ${SERVICE_NAME} service (with 30s timeout)..."
|
|
timeout 30 systemctl start "${SERVICE_NAME}" || warn "'systemctl start ${SERVICE_NAME}' timed out or failed — the service may need manual investigation"
|
|
|
|
# Brief pause so the service can initialise
|
|
sleep 2
|
|
|
|
# --- Verify ------------------------------------------------------------------
|
|
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
|
info "Service '${SERVICE_NAME}' is running."
|
|
systemctl status "${SERVICE_NAME}" --no-pager
|
|
else
|
|
warn "Service '${SERVICE_NAME}' did not start. Check logs:"
|
|
warn " journalctl -u ${SERVICE_NAME} --no-pager"
|
|
systemctl status "${SERVICE_NAME}" --no-pager || true
|
|
fi
|
|
|
|
# --- Summary -----------------------------------------------------------------
|
|
echo ""
|
|
info "================================================="
|
|
info " inBOXER Installation Complete"
|
|
info "================================================="
|
|
echo ""
|
|
info " Install directory: ${INSTALL_DIR}"
|
|
info " Binary: ${BIN_DIR}/inboxer"
|
|
info " Configuration: ${BIN_DIR}/config.yaml"
|
|
info " Prompt file: ${BIN_DIR}/prompt.txt"
|
|
info " Data (SQLite): ${DATA_DIR}/"
|
|
info " Logs: ${LOGS_DIR}/"
|
|
echo ""
|
|
info " * Edit config.yaml with your credentials before first start:"
|
|
info " sudo nano ${BIN_DIR}/config.yaml"
|
|
echo ""
|
|
info " Required settings:"
|
|
info " - ai.api_key (DeepSeek API key)"
|
|
info " - smtp.host / port (SMTP server for OTP emails)"
|
|
info " - smtp.username / pass (SMTP login credentials)"
|
|
info " - server.session_secret (change from the default)"
|
|
echo ""
|
|
if grep -q "your_deepseek_api_key_here\|your.smtp.host\|change-me-in-production" \
|
|
"${BIN_DIR}/config.yaml" 2>/dev/null; then
|
|
warn " ! config.yaml still contains placeholder values!"
|
|
warn " Edit ${BIN_DIR}/config.yaml before the service will function."
|
|
echo ""
|
|
fi
|
|
info " Service management:"
|
|
info " sudo systemctl status ${SERVICE_NAME}"
|
|
info " sudo systemctl restart ${SERVICE_NAME}"
|
|
info " sudo systemctl stop ${SERVICE_NAME}"
|
|
info " sudo journalctl -u ${SERVICE_NAME} -f"
|
|
echo ""
|
|
info " Web interface: http://$(hostname -s 2>/dev/null || echo "localhost"):8080"
|
|
info "================================================="
|
|
echo ""
|