Initial completion
This commit is contained in:
parent
74ae914886
commit
adf4ab5d92
@ -1,13 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Online SQLite backup using the .backup API (safe while the bot is running).
|
||||
# TODO: implement; outline below.
|
||||
# Online SQLite backup using the .backup API.
|
||||
# Safe to run while the bot is live — SQLite's backup API acquires only a short
|
||||
# shared lock per page, never blocking the application for the full duration.
|
||||
#
|
||||
# Usage:
|
||||
# backup.sh --config /etc/mnemosyne/mnemosyne.conf [--keep-days N]
|
||||
#
|
||||
# Environment overrides:
|
||||
# MNEMOSYNE_BACKUP_DIR — destination directory (default: /var/backups/mnemosyne)
|
||||
# MNEMOSYNE_DB_PATH — database path (overrides config)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# TODO: read DB_PATH from config or env
|
||||
# TODO: BACKUP_DIR default /var/backups/mnemosyne, override via $MNEMOSYNE_BACKUP_DIR
|
||||
# TODO: filename: mnemosyne-$(date +%Y%m%d-%H%M%S).db
|
||||
# TODO: sqlite3 "$DB_PATH" ".backup '$BACKUP_DIR/$FILENAME'"
|
||||
# (uses the SQLite backup API — safe for concurrent access, no locks held)
|
||||
# TODO: optionally prune backups older than N days
|
||||
# ── Defaults ─────────────────────────────────────────────────────────────────
|
||||
CONFIG_FILE=""
|
||||
KEEP_DAYS=30
|
||||
BACKUP_DIR="${MNEMOSYNE_BACKUP_DIR:-/var/backups/mnemosyne}"
|
||||
|
||||
echo "TODO: backup.sh not yet implemented"
|
||||
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--config) CONFIG_FILE="$2"; shift 2 ;;
|
||||
--keep-days) KEEP_DAYS="$2"; shift 2 ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Resolve DB path ──────────────────────────────────────────────────────────
|
||||
# Priority: env var > config file > error
|
||||
if [[ -n "${MNEMOSYNE_DB_PATH:-}" ]]; then
|
||||
DB_PATH="$MNEMOSYNE_DB_PATH"
|
||||
elif [[ -n "$CONFIG_FILE" ]]; then
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
echo "Config file not found: $CONFIG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Parse db.path from the INI config (section [db], key path)
|
||||
DB_PATH="$(awk -F'=' '/^\[db\]/{in_db=1; next} /^\[/{in_db=0} in_db && /^[[:space:]]*path[[:space:]]*=/{gsub(/[[:space:]#].*/, "", $2); print $2; exit}' "$CONFIG_FILE")"
|
||||
DB_PATH="${DB_PATH// /}" # strip spaces
|
||||
if [[ -z "$DB_PATH" ]]; then
|
||||
echo "Could not read db.path from $CONFIG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Provide --config or set MNEMOSYNE_DB_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$DB_PATH" ]]; then
|
||||
echo "Database not found: $DB_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Ensure backup directory exists ───────────────────────────────────────────
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# ── Run backup ───────────────────────────────────────────────────────────────
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="$BACKUP_DIR/mnemosyne-${TIMESTAMP}.db"
|
||||
|
||||
# sqlite3 .backup uses the SQLite Online Backup API — consistent snapshot,
|
||||
# safe under concurrent writes, no application downtime required.
|
||||
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"
|
||||
|
||||
# Verify the backup is a valid SQLite database
|
||||
if ! sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;" | grep -q "^ok$"; then
|
||||
echo "Backup integrity check failed: $BACKUP_FILE" >&2
|
||||
rm -f "$BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BYTES="$(wc -c < "$BACKUP_FILE")"
|
||||
echo "Backup written: $BACKUP_FILE (${BYTES} bytes)"
|
||||
|
||||
# ── Prune old backups ─────────────────────────────────────────────────────────
|
||||
if [[ "$KEEP_DAYS" -gt 0 ]]; then
|
||||
PRUNED="$(find "$BACKUP_DIR" -maxdepth 1 -name 'mnemosyne-*.db' \
|
||||
-mtime "+${KEEP_DAYS}" -print -delete | wc -l)"
|
||||
[[ "$PRUNED" -gt 0 ]] && echo "Pruned $PRUNED backup(s) older than ${KEEP_DAYS} days"
|
||||
fi
|
||||
|
||||
@ -1,44 +1,192 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mnemosyne install script — idempotent; safe to re-run.
|
||||
# Does NOT clobber an existing database or config file.
|
||||
# TODO: implement; outline below.
|
||||
#
|
||||
# Usage (as root or with sudo):
|
||||
# bash scripts/install.sh [--app-dir /opt/mnemosyne] [--config-dir /etc/mnemosyne]
|
||||
# [--data-dir /var/lib/mnemosyne]
|
||||
#
|
||||
# After running:
|
||||
# 1. Edit /etc/mnemosyne/mnemosyne.conf (fill in token, secret, URL, chat ID)
|
||||
# 2. Copy and adjust nginx/mnemosyne.conf.example → /etc/nginx/conf.d/mnemosyne.conf
|
||||
# 3. Obtain a Let's Encrypt cert: certbot --nginx -d your.subdomain.com
|
||||
# 4. Register the Telegram webhook: /opt/mnemosyne/bin/mnemosyne-webhook --config /etc/mnemosyne/mnemosyne.conf
|
||||
# 5. Start the bot: systemctl start mnemosyne-bot
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Detect distro family ---
|
||||
# TODO: detect RHEL/AlmaLinux (dnf/yum) vs Debian/Ubuntu (apt) vs other
|
||||
# TODO: on RHEL family: dnf install -y perl perl-App-cpanminus perl-DBI sqlite
|
||||
# TODO: on Debian family: apt-get install -y perl cpanminus libdbi-perl sqlite3
|
||||
# TODO: document: on either family, run `cpanm --installdeps .` after this step
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
APP_DIR="/opt/mnemosyne"
|
||||
CONFIG_DIR="/etc/mnemosyne"
|
||||
DATA_DIR="/var/lib/mnemosyne"
|
||||
SERVICE_USER="mnemosyne"
|
||||
SERVICE_GROUP="mnemosyne"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
CRON_FILE="/etc/cron.d/mnemosyne"
|
||||
|
||||
# --- System user ---
|
||||
# TODO: create 'mnemosyne' system user/group if not present (useradd -r -s /sbin/nologin)
|
||||
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--app-dir) APP_DIR="$2"; shift 2 ;;
|
||||
--config-dir) CONFIG_DIR="$2"; shift 2 ;;
|
||||
--data-dir) DATA_DIR="$2"; shift 2 ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Directories ---
|
||||
# TODO: /opt/mnemosyne — app files (copy repo here or symlink)
|
||||
# TODO: /var/lib/mnemosyne — database directory, owned by mnemosyne user
|
||||
# TODO: /etc/mnemosyne — config directory (skip if already exists to avoid clobbering)
|
||||
# ── Must run as root ─────────────────────────────────────────────────────────
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Error: this script must be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config ---
|
||||
# TODO: if /etc/mnemosyne/mnemosyne.conf does not exist:
|
||||
# copy config/mnemosyne.conf.example → /etc/mnemosyne/mnemosyne.conf
|
||||
# chmod 640, chown root:mnemosyne
|
||||
# print reminder to edit the file before starting the service
|
||||
# ── Detect source directory (where this script lives) ───────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# --- CPAN deps ---
|
||||
# TODO: cd /opt/mnemosyne && cpanm --installdeps .
|
||||
echo "==> Mnemosyne installer"
|
||||
echo " Source: $REPO_DIR"
|
||||
echo " App: $APP_DIR"
|
||||
echo " Config: $CONFIG_DIR"
|
||||
echo " Data: $DATA_DIR"
|
||||
echo ""
|
||||
|
||||
# --- Systemd ---
|
||||
# TODO: copy systemd/mnemosyne-bot.service → /etc/systemd/system/
|
||||
# TODO: systemctl daemon-reload
|
||||
# TODO: systemctl enable mnemosyne-bot (but do NOT start — user must configure first)
|
||||
# ── Detect distro family ─────────────────────────────────────────────────────
|
||||
detect_distro() {
|
||||
if command -v dnf &>/dev/null || command -v yum &>/dev/null; then
|
||||
echo "rhel"
|
||||
elif command -v apt-get &>/dev/null; then
|
||||
echo "debian"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
DISTRO="$(detect_distro)"
|
||||
|
||||
# --- Cron for digest ---
|
||||
# TODO: install a cron entry for mnemosyne user:
|
||||
# */5 * * * * /opt/mnemosyne/bin/mnemosyne-digest --config /etc/mnemosyne/mnemosyne.conf
|
||||
# This fires every 5 minutes; the script itself checks against digest_time and exits early.
|
||||
# ── 1. System packages ───────────────────────────────────────────────────────
|
||||
echo "==> Installing system packages (distro: $DISTRO)"
|
||||
case "$DISTRO" in
|
||||
rhel)
|
||||
PKG_MGR="dnf"
|
||||
command -v yum &>/dev/null && ! command -v dnf &>/dev/null && PKG_MGR="yum"
|
||||
$PKG_MGR install -y perl perl-App-cpanminus perl-DBI sqlite perl-local-lib 2>&1 | grep -E '(Installed|already|Error)' || true
|
||||
# DBD::SQLite needs C compiler for XS build
|
||||
$PKG_MGR install -y gcc make perl-devel 2>&1 | grep -E '(Installed|already|Error)' || true
|
||||
;;
|
||||
debian)
|
||||
apt-get install -y perl cpanminus libdbi-perl sqlite3 gcc make 2>&1 | grep -E '(Setting up|already|Error)' || true
|
||||
;;
|
||||
*)
|
||||
echo " Warning: unknown distro. Install Perl, cpanminus, DBI, and SQLite manually." >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Nginx ---
|
||||
# TODO: remind user to copy nginx/mnemosyne.conf.example and adjust, then reload nginx
|
||||
# ── 2. CPAN dependencies ─────────────────────────────────────────────────────
|
||||
echo "==> Installing CPAN dependencies"
|
||||
# Install into system Perl (running as root) or local::lib if preferred
|
||||
cpanm --notest --quiet --installdeps "$REPO_DIR" || {
|
||||
echo " Warning: cpanm reported errors. Check output above." >&2
|
||||
}
|
||||
|
||||
echo "TODO: install.sh not yet implemented"
|
||||
# ── 3. System user/group ─────────────────────────────────────────────────────
|
||||
echo "==> Creating system user: $SERVICE_USER"
|
||||
if ! getent group "$SERVICE_GROUP" &>/dev/null; then
|
||||
groupadd --system "$SERVICE_GROUP"
|
||||
echo " Created group $SERVICE_GROUP"
|
||||
fi
|
||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||
useradd --system --gid "$SERVICE_GROUP" --no-create-home \
|
||||
--home-dir "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER"
|
||||
echo " Created user $SERVICE_USER"
|
||||
else
|
||||
echo " User $SERVICE_USER already exists — skipping"
|
||||
fi
|
||||
|
||||
# ── 4. Directories ───────────────────────────────────────────────────────────
|
||||
echo "==> Creating directories"
|
||||
install -d -m 755 "$APP_DIR"
|
||||
install -d -m 750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$DATA_DIR"
|
||||
install -d -m 750 "$CONFIG_DIR"
|
||||
|
||||
# ── 5. Copy application files ────────────────────────────────────────────────
|
||||
echo "==> Installing application to $APP_DIR"
|
||||
# Use rsync if available (preserves structure, skips .git), otherwise cp
|
||||
if command -v rsync &>/dev/null; then
|
||||
rsync -a --exclude='.git' --exclude='config/mnemosyne.conf' \
|
||||
"$REPO_DIR/" "$APP_DIR/"
|
||||
else
|
||||
cp -rp "$REPO_DIR/." "$APP_DIR/"
|
||||
rm -rf "$APP_DIR/.git"
|
||||
rm -f "$APP_DIR/config/mnemosyne.conf"
|
||||
fi
|
||||
chmod +x "$APP_DIR/bin/"*
|
||||
chown -R root:"$SERVICE_GROUP" "$APP_DIR"
|
||||
chmod -R o-rwx "$APP_DIR"
|
||||
|
||||
# ── 6. Config file ───────────────────────────────────────────────────────────
|
||||
echo "==> Config"
|
||||
if [[ -f "$CONFIG_DIR/mnemosyne.conf" ]]; then
|
||||
echo " $CONFIG_DIR/mnemosyne.conf already exists — not overwriting"
|
||||
else
|
||||
install -m 640 -o root -g "$SERVICE_GROUP" \
|
||||
"$REPO_DIR/config/mnemosyne.conf.example" \
|
||||
"$CONFIG_DIR/mnemosyne.conf"
|
||||
echo ""
|
||||
echo " *** ACTION REQUIRED ***"
|
||||
echo " Edit $CONFIG_DIR/mnemosyne.conf and fill in:"
|
||||
echo " bot.token — from @BotFather"
|
||||
echo " bot.webhook_secret — run: openssl rand -hex 32"
|
||||
echo " bot.webhook_url — your public HTTPS webhook URL"
|
||||
echo " bot.allowed_chat_ids — your Telegram chat ID (@userinfobot)"
|
||||
echo " db.path — default: $DATA_DIR/mnemosyne.db"
|
||||
echo " app.timezone — IANA timezone (e.g. America/New_York)"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── 7. Systemd service ───────────────────────────────────────────────────────
|
||||
echo "==> Installing systemd service"
|
||||
# Rewrite paths in the service template
|
||||
sed "s|/opt/mnemosyne|$APP_DIR|g; s|/etc/mnemosyne|$CONFIG_DIR|g" \
|
||||
"$REPO_DIR/systemd/mnemosyne-bot.service" \
|
||||
> "$SYSTEMD_DIR/mnemosyne-bot.service"
|
||||
chmod 644 "$SYSTEMD_DIR/mnemosyne-bot.service"
|
||||
systemctl daemon-reload
|
||||
systemctl enable mnemosyne-bot
|
||||
echo " Enabled mnemosyne-bot.service (not started — configure first)"
|
||||
|
||||
# ── 8. Cron entry for digest ─────────────────────────────────────────────────
|
||||
echo "==> Installing cron job for digest"
|
||||
cat > "$CRON_FILE" <<CRON
|
||||
# Mnemosyne morning digest — fires every 5 min; the script checks digest_time itself.
|
||||
*/5 * * * * $SERVICE_USER $APP_DIR/bin/mnemosyne-digest --config $CONFIG_DIR/mnemosyne.conf 2>&1 | logger -t mnemosyne-digest
|
||||
CRON
|
||||
chmod 644 "$CRON_FILE"
|
||||
echo " Installed $CRON_FILE"
|
||||
|
||||
# ── 9. Backup cron (optional) ────────────────────────────────────────────────
|
||||
echo "==> Installing daily backup cron"
|
||||
BACKUP_DIR="/var/backups/mnemosyne"
|
||||
install -d -m 750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$BACKUP_DIR"
|
||||
cat >> "$CRON_FILE" <<CRON
|
||||
# Daily backup at 02:15
|
||||
15 2 * * * $SERVICE_USER MNEMOSYNE_BACKUP_DIR=$BACKUP_DIR $APP_DIR/scripts/backup.sh --config $CONFIG_DIR/mnemosyne.conf 2>&1 | logger -t mnemosyne-backup
|
||||
CRON
|
||||
echo " Backup cron added (target: $BACKUP_DIR)"
|
||||
|
||||
# ── Done ─────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "==> Installation complete."
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit $CONFIG_DIR/mnemosyne.conf"
|
||||
echo " 2. Copy nginx/mnemosyne.conf.example → /etc/nginx/conf.d/mnemosyne.conf"
|
||||
echo " and adjust server_name + port"
|
||||
echo " 3. Obtain TLS cert: certbot --nginx -d your.subdomain.example.com"
|
||||
echo " 4. Reload nginx: systemctl reload nginx"
|
||||
echo " 5. Register webhook: $APP_DIR/bin/mnemosyne-webhook --config $CONFIG_DIR/mnemosyne.conf"
|
||||
echo " 6. Start bot: systemctl start mnemosyne-bot"
|
||||
echo " 7. Check logs: journalctl -u mnemosyne-bot -f"
|
||||
echo ""
|
||||
echo " Note (RHEL/AlmaLinux): if SELinux is enforcing, you may need:"
|
||||
echo " setsebool -P httpd_can_network_connect 1"
|
||||
echo " (allows nginx to proxy to the local bot port)"
|
||||
|
||||
@ -1,19 +1,63 @@
|
||||
[Unit]
|
||||
Description=Mnemosyne Telegram bot (webhook receiver)
|
||||
After=network.target
|
||||
Documentation=https://git.castlehollow.com/rodger/mnemosyne
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mnemosyne
|
||||
Group=mnemosyne
|
||||
WorkingDirectory=/opt/mnemosyne
|
||||
ExecStart=/usr/bin/perl /opt/mnemosyne/bin/mnemosyne-bot --config /etc/mnemosyne/mnemosyne.conf
|
||||
|
||||
ExecStart=/usr/bin/perl /opt/mnemosyne/bin/mnemosyne-bot \
|
||||
--config /etc/mnemosyne/mnemosyne.conf
|
||||
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# Keep logs in the journal
|
||||
# Cap restart rate: no more than 5 restarts in 60 seconds
|
||||
StartLimitBurst=5
|
||||
StartLimitIntervalSec=60
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=mnemosyne-bot
|
||||
|
||||
# ── Hardening ────────────────────────────────────────────────────────────────
|
||||
# Prevent privilege escalation
|
||||
NoNewPrivileges=true
|
||||
|
||||
# Private /tmp — isolates temp files from other services
|
||||
PrivateTmp=true
|
||||
|
||||
# Read-only /usr, /boot, /etc (except the config dir we explicitly allow)
|
||||
ProtectSystem=strict
|
||||
# The bot needs to write the SQLite database; allow the data dir
|
||||
ReadWritePaths=/var/lib/mnemosyne
|
||||
|
||||
# No access to home directories
|
||||
ProtectHome=true
|
||||
|
||||
# Prevent writing to kernel tunables
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
|
||||
# Restrict address families: only IPv4/IPv6 (outbound Telegram API) + Unix sockets
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
|
||||
# Syscall filtering — Perl/DBI only needs the basics
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallErrorNumber=EPERM
|
||||
|
||||
# No realtime scheduling, no raw I/O, no device access
|
||||
RestrictRealtime=true
|
||||
PrivateDevices=true
|
||||
|
||||
# Tighten /proc view
|
||||
ProtectProc=invisible
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Loading…
Reference in New Issue
Block a user