- Add ReadWritePaths=/opt/inboxer/data /opt/inboxer/logs to the systemd unit so ProtectSystem=full doesn't block SQLite DB and log file creation - Wrap systemctl start with timeout 30s so the script never hangs forever if the service start job blocks
240 lines
7.8 KiB
Bash
Executable file
240 lines
7.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 ----------------------------------------------
|
|
info "Creating directories under ${INSTALL_DIR}..."
|
|
mkdir -p "${BIN_DIR}" "${DATA_DIR}" "${LOGS_DIR}"
|
|
|
|
# --- Download helper ---------------------------------------------------------
|
|
download() {
|
|
local src_url="$1"
|
|
local dest_path="$2"
|
|
local mode="$3"
|
|
|
|
local tmpfile
|
|
tmpfile="$(mktemp)"
|
|
|
|
if [[ "${DOWNLOADER}" == curl* ]]; then
|
|
curl -sSL "${src_url}" -o "${tmpfile}"
|
|
else
|
|
wget -q -O "${tmpfile}" "${src_url}"
|
|
fi
|
|
|
|
# Check that the downloaded content is non-empty and not an HTML error page
|
|
if [[ ! -s "${tmpfile}" ]]; then
|
|
rm -f "${tmpfile}"
|
|
error "Downloaded empty file from ${src_url}"
|
|
return 1
|
|
fi
|
|
|
|
# Gitea returns HTML on 404; detect by checking first bytes
|
|
if head -c 100 "${tmpfile}" | grep -qi "<html\|<!DOCTYPE"; then
|
|
rm -f "${tmpfile}"
|
|
error "File not found at ${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
|
|
|
|
# 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 ""
|