- Binary output changed from 'receiptnext' to 'app' - Release assets renamed to app-linux-amd64 / app-linux-arm64 - install.sh, Makefile, README.md updated to reference 'app' - contrib/receiptnext.service uses /opt/receiptnext/app - Systemd service updated to ExecStart=/opt/receiptnext/app
372 lines
14 KiB
Bash
Executable file
372 lines
14 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# ReceiptNext Installer
|
|
# =====================
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://git.lohmar.co.uk/cclohmar/ReceiptNext/raw/branch/main/install.sh | bash
|
|
# bash install.sh — Full install
|
|
# bash install.sh -update — Pull latest, rebuild, restart
|
|
#
|
|
# Installs to /opt/receiptnext/ and sets up a systemd service.
|
|
# Uses sudo internally only for operations that require it.
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
INSTALL_DIR="/opt/receiptnext"
|
|
SERVICE_NAME="receiptnext"
|
|
REPO_URL="https://git.lohmar.co.uk/cclohmar/ReceiptNext.git"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|
err() { echo -e "${RED}[✗]${NC} $1"; }
|
|
ask() { echo -en "${CYAN}→${NC} $1"; }
|
|
|
|
# Helper: run a command with sudo if not already root
|
|
sudo_if() {
|
|
if [ "$EUID" -eq 0 ]; then
|
|
"$@"
|
|
else
|
|
sudo "$@"
|
|
fi
|
|
}
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# UPDATE MODE
|
|
# ──────────────────────────────────────────────────────────────────
|
|
if [ "${1:-}" = "-update" ]; then
|
|
echo ""
|
|
echo " ╔═══════════════════════════════════════╗"
|
|
echo " ║ ReceiptNext — Update Mode ║"
|
|
echo " ╚═══════════════════════════════════════╝"
|
|
echo ""
|
|
|
|
if [ ! -d "$INSTALL_DIR" ]; then
|
|
err "$INSTALL_DIR does not exist. Run install.sh without -update first."
|
|
exit 1
|
|
fi
|
|
|
|
cd "$INSTALL_DIR"
|
|
|
|
if [ -d .git ]; then
|
|
info "Pulling latest code..."
|
|
sudo_if git pull
|
|
else
|
|
warn "Not a git repository — re-cloning..."
|
|
cd /tmp
|
|
rm -rf receiptnext-update
|
|
sudo_if git clone "$REPO_URL" receiptnext-update
|
|
sudo_if rsync -a --delete receiptnext-update/ "$INSTALL_DIR/"
|
|
rm -rf receiptnext-update
|
|
cd "$INSTALL_DIR"
|
|
fi
|
|
|
|
info "Rebuilding binary..."
|
|
if command -v go &>/dev/null; then
|
|
CGO_ENABLED=0 go build -ldflags="-s -w" -o app .
|
|
else
|
|
warn "Go not found — downloading pre-built binary..."
|
|
TAG=$(curl -fsSL https://git.lohmar.co.uk/api/v1/repos/cclohmar/ReceiptNext/releases/latest \
|
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','v1.0.0'))" 2>/dev/null || echo "v1.0.0")
|
|
ARCH="$(uname -m)"; [ "$ARCH" = "x86_64" ] && ARCH="amd64"; [ "$ARCH" = "aarch64" ] && ARCH="arm64"
|
|
sudo_if curl -fsSL -o app .https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/${TAG}/app-linux-${ARCH}"
|
|
sudo_if chmod +x receiptnext
|
|
fi
|
|
|
|
info "Restarting service..."
|
|
systemctl restart "$SERVICE_NAME"
|
|
sleep 2
|
|
|
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
|
info "Update complete — ReceiptNext is running"
|
|
else
|
|
warn "Service did not start. Check: systemctl status $SERVICE_NAME"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# FRESH INSTALL
|
|
# ──────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo " ╔═══════════════════════════════════════╗"
|
|
echo " ║ ReceiptNext Installer ║"
|
|
echo " ╚═══════════════════════════════════════╝"
|
|
echo ""
|
|
|
|
# ── 1. Clone repo ────────────────────────────────────────────────
|
|
if [ -d "$INSTALL_DIR" ]; then
|
|
warn "$INSTALL_DIR already exists — pulling latest..."
|
|
cd "$INSTALL_DIR"
|
|
sudo_if git pull
|
|
else
|
|
info "Cloning repository to $INSTALL_DIR..."
|
|
sudo_if git clone "$REPO_URL" "$INSTALL_DIR"
|
|
cd "$INSTALL_DIR"
|
|
fi
|
|
|
|
# ── 2. Build or download binary ──────────────────────────────────
|
|
ARCH="$(uname -m)"
|
|
case "$ARCH" in
|
|
x86_64) ARCH="amd64" ;;
|
|
aarch64) ARCH="arm64" ;;
|
|
*) err "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
esac
|
|
|
|
if command -v go &>/dev/null; then
|
|
info "Building binary from source (pure Go, no CGO)..."
|
|
CGO_ENABLED=0 go build -ldflags="-s -w" -o app .
|
|
else
|
|
info "Go not found — downloading pre-built binary..."
|
|
TAG=$(curl -fsSL https://git.lohmar.co.uk/api/v1/repos/cclohmar/ReceiptNext/releases/latest \
|
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','v1.0.0'))" 2>/dev/null || echo "v1.0.0")
|
|
URL="https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/${TAG}/app-linux-${ARCH}"
|
|
sudo_if curl -fsSL -o app .$URL"
|
|
sudo_if chmod +x receiptnext
|
|
fi
|
|
info "Binary ready: $INSTALL_DIR/app
|
|
|
|
# ── 3. Create runtime directories ────────────────────────────────
|
|
sudo_if mkdir -p storage
|
|
sudo_if chmod 755 templates static storage
|
|
|
|
# ── 4. AI Provider ───────────────────────────────────────────────
|
|
echo ""
|
|
echo " ── AI Provider ──"
|
|
echo " Which AI should process receipt images?"
|
|
echo " 1) Google Gemini (cloud API, needs API key)"
|
|
echo " 2) OpenAI / Compatible (OpenAI, Perplexity, Groq, etc.)"
|
|
echo " 3) Ollama (local, installs automatically)"
|
|
echo ""
|
|
ask " Choose [1-3] (default: 1): "
|
|
read -r AI_CHOICE
|
|
AI_CHOICE="${AI_CHOICE:-1}"
|
|
|
|
case "$AI_CHOICE" in
|
|
2)
|
|
AI_PROVIDER="openai"
|
|
ask " OpenAI API key: "
|
|
read -r OPENAI_API_KEY
|
|
ask " Model [gpt-4o-mini]: "
|
|
read -r AI_MODEL
|
|
AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
|
|
ask " Base URL [https://api.openai.com/v1]: "
|
|
read -r AI_BASE_URL
|
|
AI_BASE_URL="${AI_BASE_URL:-https://api.openai.com/v1}"
|
|
;;
|
|
3)
|
|
AI_PROVIDER="ollama"
|
|
AI_MODEL="qwen3.5:2b"
|
|
# Ollama's install script requires zstd
|
|
if ! command -v zstd &>/dev/null; then
|
|
info "Installing zstd (required by Ollama)..."
|
|
if command -v apt-get &>/dev/null; then
|
|
sudo_if apt-get update -qq && apt-get install -y -qq zstd 2>&1 | tail -1
|
|
elif command -v dnf &>/dev/null; then
|
|
sudo_if dnf install -y zstd 2>&1 | tail -1
|
|
elif command -v yum &>/dev/null; then
|
|
sudo_if yum install -y zstd 2>&1 | tail -1
|
|
elif command -v apk &>/dev/null; then
|
|
sudo_if apk add zstd 2>&1 | tail -1
|
|
else
|
|
warn "Please install zstd manually, then re-run install.sh"
|
|
fi
|
|
fi
|
|
info "Installing Ollama..."
|
|
if ! command -v ollama &>/dev/null; then
|
|
sudo_if sh -c "$(curl -fsSL https://ollama.com/install.sh)" 2>&1 | tail -5
|
|
else
|
|
info "Ollama already installed"
|
|
fi
|
|
# Wait for Ollama to be ready
|
|
for i in $(seq 1 10); do
|
|
if ollama --version &>/dev/null; then break; fi
|
|
sleep 2
|
|
done
|
|
info "Pulling model $AI_MODEL (this may take a while)..."
|
|
sudo_if ollama pull "$AI_MODEL" 2>&1 | tail -3
|
|
AI_BASE_URL="http://localhost:11434"
|
|
;;
|
|
*)
|
|
AI_PROVIDER="gemini"
|
|
ask " Gemini API key: "
|
|
read -r GEMINI_API_KEY
|
|
;;
|
|
esac
|
|
|
|
# ── 5. SMTP Configuration ────────────────────────────────────────
|
|
echo ""
|
|
echo " ── Email (SMTP) ──"
|
|
echo " Required for sending OTP codes and expense reports."
|
|
echo " Leave blank to skip (OTP codes will be logged to console)."
|
|
echo ""
|
|
ask " SMTP host: "
|
|
read -r SMTP_HOST
|
|
if [ -n "$SMTP_HOST" ]; then
|
|
ask " SMTP port [587]: "
|
|
read -r SMTP_PORT
|
|
SMTP_PORT="${SMTP_PORT:-587}"
|
|
ask " SMTP user: "
|
|
read -r SMTP_USER
|
|
ask " SMTP password: "
|
|
read -r SMTP_PASS
|
|
fi
|
|
|
|
# ── 6. General settings ──────────────────────────────────────────
|
|
echo ""
|
|
echo " ── General ──"
|
|
echo " If the app is behind a proxy (e.g. dev.lohmar.co.uk), enter the domain."
|
|
echo " Otherwise leave blank to use localhost."
|
|
echo ""
|
|
ask " Domain name [localhost]: "
|
|
read -r DOMAIN
|
|
DOMAIN="${DOMAIN:-localhost}"
|
|
ask " HTTPS? (y/N): "
|
|
read -r USE_HTTPS
|
|
if [[ "$USE_HTTPS" =~ ^[Yy]$ ]]; then
|
|
BASE_URL="https://${DOMAIN}"
|
|
else
|
|
BASE_URL="http://${DOMAIN}"
|
|
fi
|
|
ask " Web server port [8080]: "
|
|
read -r PORT
|
|
PORT="${PORT:-8080}"
|
|
if [ "$DOMAIN" = "localhost" ]; then
|
|
BASE_URL="http://localhost:${PORT}"
|
|
fi
|
|
|
|
# ── 7. Write .env ────────────────────────────────────────────────
|
|
info "Creating .env..."
|
|
sudo_if tee "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
|
# ReceiptNext Configuration
|
|
# Generated by install.sh on $(date)
|
|
|
|
PORT=${PORT}
|
|
BASE_URL=${BASE_URL}
|
|
AI_PROVIDER=${AI_PROVIDER}
|
|
ENVEOF
|
|
|
|
case "$AI_PROVIDER" in
|
|
openai)
|
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
|
OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
AI_MODEL=${AI_MODEL}
|
|
AI_BASE_URL=${AI_BASE_URL}
|
|
ENVEOF
|
|
;;
|
|
ollama)
|
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
|
AI_MODEL=${AI_MODEL}
|
|
AI_BASE_URL=${AI_BASE_URL}
|
|
ENVEOF
|
|
;;
|
|
gemini)
|
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
|
GEMINI_API_KEY=${GEMINI_API_KEY}
|
|
ENVEOF
|
|
;;
|
|
esac
|
|
|
|
if [ -n "$SMTP_HOST" ]; then
|
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
|
SMTP_HOST=${SMTP_HOST}
|
|
SMTP_PORT=${SMTP_PORT}
|
|
SMTP_USER=${SMTP_USER}
|
|
SMTP_PASS=${SMTP_PASS}
|
|
ENVEOF
|
|
fi
|
|
|
|
sudo_if chmod 600 "$INSTALL_DIR/.env"
|
|
info "Configuration saved to $INSTALL_DIR/.env"
|
|
|
|
# ── 8. Determine app user ──────────────────────────────────────────
|
|
# Detect the real user (the one who invoked the script, not root via sudo)
|
|
APP_USER="${SUDO_USER:-$(whoami)}"
|
|
# If still root, create a dedicated system user
|
|
if [ "$APP_USER" = "root" ]; then
|
|
APP_USER="${SERVICE_NAME}"
|
|
if ! id -u "${APP_USER}" &>/dev/null 2>&1; then
|
|
info "Creating system user '${APP_USER}'..."
|
|
sudo_if useradd --system --no-create-home --home-dir "${INSTALL_DIR}" --shell /usr/sbin/nologin "${APP_USER}"
|
|
fi
|
|
else
|
|
info "Using existing user '${APP_USER}'"
|
|
# Add user to ollama group if it exists (so they can run ollama CLI)
|
|
if getent group ollama &>/dev/null; then
|
|
sudo_if usermod -aG ollama "${APP_USER}" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# ── 9. Set ownership ───────────────────────────────────────────────
|
|
sudo_if chown -R "${APP_USER}:${APP_USER}" "${INSTALL_DIR}"
|
|
sudo_if chmod 755 "${INSTALL_DIR}" "${INSTALL_DIR}/templates" "${INSTALL_DIR}/static"
|
|
# Storage needs write permission for uploaded receipts
|
|
sudo_if chmod 775 "${INSTALL_DIR}/storage"
|
|
|
|
# ── 10. Systemd service ────────────────────────────────────────────
|
|
info "Creating systemd service..."
|
|
sudo_if tee "/etc/systemd/system/${SERVICE_NAME}.service" > /dev/null << SERVEOF
|
|
[Unit]
|
|
Description=ReceiptNext — AI-Powered Expense Tracker
|
|
Documentation=https://git.lohmar.co.uk/cclohmar/ReceiptNext
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${APP_USER}
|
|
Group=${APP_USER}
|
|
WorkingDirectory=${INSTALL_DIR}
|
|
ExecStart=${INSTALL_DIR}/app
|
|
Restart=always
|
|
RestartSec=5
|
|
EnvironmentFile=${INSTALL_DIR}/.env
|
|
StandardOutput=append:/var/log/receiptnext.log
|
|
StandardError=append:/var/log/receiptnext.log
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SERVEOF
|
|
|
|
sudo_if systemctl daemon-reload
|
|
sudo_if systemctl enable "${SERVICE_NAME}"
|
|
info "Service created: /etc/systemd/system/${SERVICE_NAME}.service"
|
|
|
|
# ── 9. Start ─────────────────────────────────────────────────────
|
|
info "Starting ReceiptNext..."
|
|
sudo_if systemctl restart "${SERVICE_NAME}" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
echo ""
|
|
echo " ╔═══════════════════════════════════════╗"
|
|
echo " ║ Installation Complete! ║"
|
|
echo " ╚═══════════════════════════════════════╝"
|
|
echo ""
|
|
echo " Install: $INSTALL_DIR"
|
|
echo " Binary: $INSTALL_DIR/receiptnext"
|
|
echo " Config: $INSTALL_DIR/.env"
|
|
echo " Templates: $INSTALL_DIR/templates/"
|
|
echo " Static: $INSTALL_DIR/static/"
|
|
echo " Storage: $INSTALL_DIR/storage/"
|
|
echo " Service: systemctl status $SERVICE_NAME"
|
|
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
|
echo ""
|
|
|
|
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
|
info "ReceiptNext is running on port ${PORT}"
|
|
echo " Open http://localhost:${PORT} (or your server IP)"
|
|
else
|
|
warn "Service did not start. Check: systemctl status ${SERVICE_NAME}"
|
|
journalctl -u "${SERVICE_NAME}" -n 20 --no-pager
|
|
fi
|
|
|
|
echo ""
|
|
echo " ── Update ──"
|
|
echo " To update later: sudo ./install.sh -update"
|
|
echo ""
|