#!/bin/bash set -e APP_DIR="/opt/mbs-panel" ENV_FILE="$APP_DIR/.env" usage() { cat << 'EOF' mbs — управление MBS Panel mbs pass [новый_пароль] сменить пароль админ-панели (без аргумента — сгенерировать случайный) mbs status статус всех сервисов (bot, api, xray, nginx) mbs restart перезапустить всё (bot, api, xray, reload nginx) mbs logs [bot|api|xray] последние строки лога (по умолчанию api) mbs domain показать текущий домен панели mbs backup полная копия панели (база, .env, ручные правки, конфиг Xray) в /root/mbs-backups mbs update [ссылка] обновить код и перезапустить (не трогает .env и базу, перед этим сам делает копию). Без ссылки: своё зеркало -> api.savsis.xyz -> GitHub. Со ссылкой на git-репозиторий (зеркало) — берёт обновление оттуда, один раз mbs mirror [ссылка|off] показать / запомнить / убрать своё зеркало, которое mbs update проверяет первым EOF } cmd_pass() { local new_pass="$1" if [ -z "$new_pass" ]; then new_pass=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 14) fi if grep -q "^ADMIN_PANEL_PASSWORD=" "$ENV_FILE"; then sed -i "s#^ADMIN_PANEL_PASSWORD=.*#ADMIN_PANEL_PASSWORD=$new_pass#" "$ENV_FILE" else echo "ADMIN_PANEL_PASSWORD=$new_pass" >> "$ENV_FILE" fi systemctl restart mbs-api echo "Новый пароль панели: $new_pass" } cmd_status() { systemctl --no-pager status mbs-bot mbs-api xray nginx 2>&1 | grep -E "●|Active:" } cmd_restart() { systemctl restart mbs-bot mbs-api xray systemctl reload nginx 2>/dev/null || true echo "Перезапущено: bot, api, xray (+ reload nginx)." } cmd_logs() { local svc="${1:-api}" case "$svc" in bot) journalctl -u mbs-bot -n 60 --no-pager ;; api) journalctl -u mbs-api -n 60 --no-pager ;; xray) journalctl -u xray -n 60 --no-pager ;; *) echo "неизвестный сервис: $svc (bot|api|xray)"; exit 1 ;; esac } cmd_domain() { grep "^PANEL_DOMAIN=" "$ENV_FILE" } MIRROR_FILE="$APP_DIR/.update_mirror" BACKUP_DIR="${MBS_BACKUP_DIR:-/root/mbs-backups}" XRAY_CONFIG="/usr/local/etc/xray/config.json" BACKUP_FILE="" UPDATE_STASHED=0 snapshot_backup() { local stamp dbsnap file local -a targs=(--exclude=venv --exclude=__pycache__) stamp=$(date +%Y%m%d-%H%M%S) file="$BACKUP_DIR/mbs-$1-$stamp.tar.gz" dbsnap="$APP_DIR/.mbs.db.snapshot" mkdir -p "$BACKUP_DIR" || return 1 chmod 700 "$BACKUP_DIR" rm -f "$dbsnap" if [ -f "$APP_DIR/mbs.db" ] && [ -x "$APP_DIR/venv/bin/python" ] && \ "$APP_DIR/venv/bin/python" -c "import sqlite3,sys; s=sqlite3.connect(sys.argv[1]); d=sqlite3.connect(sys.argv[2]); s.backup(d); d.close(); s.close()" "$APP_DIR/mbs.db" "$dbsnap" 2>/dev/null; then targs+=(--exclude=mbs.db --exclude=mbs.db-wal --exclude=mbs.db-shm "--transform=s#\.mbs\.db\.snapshot#mbs.db#") fi if [ -f "$XRAY_CONFIG" ]; then tar czf "$file" "${targs[@]}" "$APP_DIR" "$XRAY_CONFIG" 2>/dev/null else tar czf "$file" "${targs[@]}" "$APP_DIR" 2>/dev/null fi local rc=$? rm -f "$dbsnap" if [ $rc -ne 0 ] || [ ! -s "$file" ]; then rm -f "$file" return 1 fi chmod 600 "$file" ls -1t "$BACKUP_DIR"/mbs-*.tar.gz 2>/dev/null | tail -n +6 | while read -r old; do rm -f "$old"; done BACKUP_FILE="$file" return 0 } cmd_backup() { if snapshot_backup manual; then echo "копия сохранена: $BACKUP_FILE" echo "внутри: база (консистентный снапшот), .env, код с твоими правками, конфиг Xray. Хранится 5 последних." else echo "не удалось сделать копию в $BACKUP_DIR (место на диске?)" return 1 fi } valid_url() { case "$1" in https://*|http://*|ssh://*|git@*:*) ;; *) return 1 ;; esac case "$1" in *[[:space:]]*) return 1 ;; esac return 0 } fetch_url() { git -c protocol.ext.allow=never fetch --quiet -- "$1" main 2>/dev/null } restore_stash() { if [ "$UPDATE_STASHED" = "1" ]; then UPDATE_STASHED=0 if git stash pop --quiet 2>/dev/null; then echo "ручные правки вернул на место" else echo "ручные правки остались в git stash (git stash list)" fi fi } cmd_mirror() { local url="$1" case "$url" in "") if [ -f "$MIRROR_FILE" ]; then echo "своё зеркало для обновлений: $(head -n 1 "$MIRROR_FILE")" else echo "своё зеркало не задано — mbs update берёт api.savsis.xyz, потом GitHub" fi ;; off|clear) rm -f "$MIRROR_FILE" echo "своё зеркало убрано" ;; *) if ! valid_url "$url"; then echo "это не похоже на ссылку на git-репозиторий (нужна https://..., ssh://... или git@хост:путь)" return 1 fi printf '%s\n' "$url" > "$MIRROR_FILE" echo "зеркало запомнил: $url — mbs update теперь проверяет его первым" ;; esac } cmd_update() { local arg_url="$1" target="" saved="" before="" stamp="" patch="" cd "$APP_DIR" echo "проверяю обновления..." if [ -n "$arg_url" ]; then if ! valid_url "$arg_url"; then echo "это не похоже на ссылку на git-репозиторий (нужна https://..., ssh://... или git@хост:путь)" return 1 fi if ! fetch_url "$arg_url"; then echo "не удалось получить обновления по ссылке: $arg_url" return 1 fi target=$(git rev-parse FETCH_HEAD) echo "источник: $arg_url" else if [ -f "$MIRROR_FILE" ]; then saved=$(head -n 1 "$MIRROR_FILE" | tr -d '[:space:]') fi if [ -n "$saved" ] && valid_url "$saved" && fetch_url "$saved"; then target=$(git rev-parse FETCH_HEAD) echo "источник: своё зеркало $saved" elif git fetch --quiet origin main 2>/dev/null; then target=$(git rev-parse origin/main) elif git remote | grep -q '^github$' && git fetch --quiet github main 2>/dev/null; then echo "зеркало недоступно, взял с github..." target=$(git rev-parse github/main) else echo "не удалось получить обновления ни с зеркала, ни с github" return 1 fi fi before=$(git rev-parse HEAD) if [ "$before" = "$target" ]; then echo "уже последняя версия ($before)." return 0 fi if git merge-base --is-ancestor "$target" "$before" 2>/dev/null; then echo "на сервере версия новее, чем в источнике ($before) — ничего не делаю." return 0 fi echo "текущая: $before" echo "новая: $target" if ! snapshot_backup before-update; then echo "не вышло сделать резервную копию в $BACKUP_DIR — обновление не начинаю, чтобы ничего не потерять (место на диске?)" return 1 fi echo "резервная копия перед обновлением: $BACKUP_FILE" if [ -n "$(git status --porcelain --untracked-files=no)" ]; then stamp=$(date +%Y%m%d-%H%M%S) mkdir -p "$APP_DIR/local-changes" patch="$APP_DIR/local-changes/local-changes-$stamp.patch" git diff HEAD > "$patch" if GIT_AUTHOR_NAME=mbs GIT_AUTHOR_EMAIL=mbs@localhost GIT_COMMITTER_NAME=mbs GIT_COMMITTER_EMAIL=mbs@localhost git stash push --quiet -m "mbs-update-$stamp"; then UPDATE_STASHED=1 echo "на сервере были ручные правки — убрал в сторону, ничего не потеряно:" echo " патч: $patch" echo " stash: git stash list (вернуть обратно: git stash pop)" else echo "не вышло спрятать ручные правки, остановился — разберись руками: git status" return 1 fi fi if ! git merge --ff-only "$target" --quiet 2>/dev/null; then echo "не вышло быстро обновиться (на сервере есть свои коммиты, которых нет в источнике) — разберись руками: git log" restore_stash return 1 fi echo "обновляю зависимости..." venv/bin/pip install --quiet -r requirements.txt echo "проверяю код..." if ! venv/bin/python -m py_compile *.py; then echo "новый код не проходит проверку, откатываюсь на $before..." git reset --hard "$before" --quiet venv/bin/pip install --quiet -r requirements.txt restore_stash return 1 fi echo "обновляю сам CLI..." cp "$APP_DIR/mbs" /usr/local/bin/mbs chmod +x /usr/local/bin/mbs echo "обновляю systemd-юниты..." local cpu_count api_workers cpu_count=$(nproc 2>/dev/null || echo 1) if [ "$cpu_count" -lt 2 ]; then api_workers=1 elif [ "$cpu_count" -gt 4 ]; then api_workers=4 else api_workers=$cpu_count fi cp "$APP_DIR/systemd/mbs-bot.service" /etc/systemd/system/mbs-bot.service sed "s/__WORKERS__/$api_workers/" "$APP_DIR/systemd/mbs-api.service" > /etc/systemd/system/mbs-api.service systemctl daemon-reload echo "перезапускаю..." systemctl restart mbs-bot mbs-api xray systemctl reload nginx 2>/dev/null || true sleep 2 if systemctl is-active --quiet mbs-bot && systemctl is-active --quiet mbs-api; then echo "обновлено: $before -> $(git rev-parse --short HEAD)" if [ "$UPDATE_STASHED" = "1" ]; then echo "твои ручные правки лежат в git stash и в $patch — если они нужны, посмотри git stash show -p" fi echo "если что-то пошло не так: копия $BACKUP_FILE (распаковать: tar xzf файл -C /)" if [ -n "$arg_url" ]; then echo "чтобы всегда обновляться с этого зеркала: mbs mirror $arg_url" fi else echo "сервисы не поднялись после обновления, откатываюсь на $before..." git reset --hard "$before" --quiet venv/bin/pip install --quiet -r requirements.txt systemctl restart mbs-bot mbs-api restore_stash echo "откачено обратно на $before" return 1 fi } main() { case "$1" in pass) cmd_pass "$2" ;; status) cmd_status ;; restart) cmd_restart ;; logs) cmd_logs "$2" ;; domain) cmd_domain ;; update) cmd_update "$2" ;; mirror) cmd_mirror "$2" ;; backup) cmd_backup ;; *) usage ;; esac } main "$@"; exit $?