This commit is contained in:
@@ -1,27 +1,38 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
SERVICE="bot" # имя сервиса в docker-compose
|
# === Настройки ===
|
||||||
APP_DIR="/app" # рабочая директория в контейнере
|
SERVICE="bot" # имя сервиса из docker-compose.yml
|
||||||
|
APP_DIR="/app" # рабочая директория кода в контейнере
|
||||||
HOST_DB_DIR="./db" # каталог БД на хосте
|
HOST_DB_DIR="./db" # каталог БД на хосте
|
||||||
HOST_DB_FILE="./db/bot.db" # файл БД на хосте
|
HOST_DB_FILE="./db/bot.db" # файл БД на хосте
|
||||||
DB_URL_DEFAULT="sqlite+aiosqlite:////app/db/bot.db" # единый путь БД в контейнере
|
DB_URL_DEFAULT="sqlite+aiosqlite:////app/db/bot.db" # ЕДИНЫЙ URL БД в контейнере
|
||||||
|
|
||||||
log(){ echo -e "[update.sh] $*"; }
|
log(){ echo -e "[update.sh] $*"; }
|
||||||
die(){ echo -e "[update.sh][ERROR] $*" >&2; exit 1; }
|
die(){ echo -e "[update.sh][ERROR] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Запускать из корня репо, даже если скрипт лежит в bin/
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
|
|
||||||
# --- 0) Приводим БД к ./db/bot.db и .env к единому URL ---
|
# --- Утилиты для alembic в "смонтированном" контейнере (чтобы файлы миграций попадали в репо) ---
|
||||||
|
alembic_run_mounted() {
|
||||||
|
# использование: alembic_run_mounted "upgrade head"
|
||||||
|
docker compose run --rm -T \
|
||||||
|
-v "$PWD":/app \
|
||||||
|
-w /app \
|
||||||
|
"${SERVICE}" sh -lc "alembic $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 0) Приводим БД и .env к единому виду ---
|
||||||
log "Проверка каталога БД ${HOST_DB_DIR} ..."
|
log "Проверка каталога БД ${HOST_DB_DIR} ..."
|
||||||
mkdir -p "${HOST_DB_DIR}"
|
mkdir -p "${HOST_DB_DIR}"
|
||||||
|
|
||||||
# гашим древний конфликт ./bot.db (файл/папка)
|
# Если в проекте остался прежний конфликтный объект ./bot.db — убираем/переносим
|
||||||
if [[ -d "./bot.db" ]]; then
|
if [[ -d ./bot.db ]]; then
|
||||||
log "Удаляю конфликтующую ПАПКУ ./bot.db"
|
log "Удаляю конфликтующую ПАПКУ ./bot.db"
|
||||||
rm -rf ./bot.db
|
rm -rf ./bot.db
|
||||||
fi
|
fi
|
||||||
if [[ -f "./bot.db" && ! -f "${HOST_DB_FILE}" ]]; then
|
if [[ -f ./bot.db && ! -f "${HOST_DB_FILE}" ]]; then
|
||||||
log "Переношу старый файл ./bot.db -> ${HOST_DB_FILE}"
|
log "Переношу старый файл ./bot.db -> ${HOST_DB_FILE}"
|
||||||
mv ./bot.db "${HOST_DB_FILE}"
|
mv ./bot.db "${HOST_DB_FILE}"
|
||||||
fi
|
fi
|
||||||
@@ -30,43 +41,64 @@ if [[ ! -f "${HOST_DB_FILE}" ]]; then
|
|||||||
:> "${HOST_DB_FILE}"
|
:> "${HOST_DB_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# .env: гарантируем DATABASE_URL
|
# .env: зафиксировать DATABASE_URL
|
||||||
if [[ -f .env ]]; then
|
if [[ -f .env ]]; then
|
||||||
if ! grep -q '^DATABASE_URL=' .env; then
|
if grep -q '^DATABASE_URL=' .env; then
|
||||||
log "В .env не найден DATABASE_URL — дописываю ${DB_URL_DEFAULT}"
|
|
||||||
echo "DATABASE_URL=${DB_URL_DEFAULT}" >> .env
|
|
||||||
else
|
|
||||||
# аккуратно заменим на нужный абсолютный путь, если другой
|
|
||||||
sed -i "s|^DATABASE_URL=.*$|DATABASE_URL=${DB_URL_DEFAULT}|g" .env
|
sed -i "s|^DATABASE_URL=.*$|DATABASE_URL=${DB_URL_DEFAULT}|g" .env
|
||||||
log "DATABASE_URL в .env → ${DB_URL_DEFAULT}"
|
log "DATABASE_URL в .env → ${DB_URL_DEFAULT}"
|
||||||
|
else
|
||||||
|
echo "DATABASE_URL=${DB_URL_DEFAULT}" >> .env
|
||||||
|
log "Добавил DATABASE_URL в .env → ${DB_URL_DEFAULT}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log "Создаю .env с DATABASE_URL=${DB_URL_DEFAULT}"
|
|
||||||
echo "DATABASE_URL=${DB_URL_DEFAULT}" > .env
|
echo "DATABASE_URL=${DB_URL_DEFAULT}" > .env
|
||||||
|
log "Создал .env с DATABASE_URL=${DB_URL_DEFAULT}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 1) git pull + build ---
|
# --- 1) git pull + сборка ---
|
||||||
log "git pull --rebase --autostash ..."
|
log "git pull --rebase --autostash ..."
|
||||||
git pull --rebase --autostash || die "git pull не удался"
|
git pull --rebase --autostash || die "git pull не удался"
|
||||||
|
|
||||||
log "Пересборка образа ..."
|
log "Пересборка образа ..."
|
||||||
docker compose build --no-cache
|
docker compose build --no-cache || die "docker compose build не удался"
|
||||||
|
|
||||||
# --- 2) Функция безопасного апгрейда Alembic ---
|
# --- 2) Безопасный upgrade: выравниваем БД до HEAD; при «потере» ревизии чиним alembic_version ---
|
||||||
safe_upgrade() {
|
safe_upgrade() {
|
||||||
log "alembic upgrade head ..."
|
log "alembic upgrade head ..."
|
||||||
if docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic upgrade head"; then
|
set +e
|
||||||
|
UPG_LOG="$(alembic_run_mounted 'upgrade head' 2>&1)"
|
||||||
|
RC=$?
|
||||||
|
set -e
|
||||||
|
if [[ $RC -eq 0 ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
log "upgrade head не прошёл. Пытаюсь выровнять ревизии: alembic stamp head → upgrade head"
|
|
||||||
docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic stamp head" || die "alembic stamp head провалился"
|
echo "$UPG_LOG"
|
||||||
docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic upgrade head" || die "alembic upgrade head провалился повторно"
|
if grep -q "Can't locate revision identified by" <<< "$UPG_LOG"; then
|
||||||
|
log "Обнаружена «потерянная» ревизия. Автопочинка: подшиваю БД к актуальному HEAD ..."
|
||||||
|
# Узнаём актуальный HEAD из каталога миграций
|
||||||
|
set +e
|
||||||
|
HEADREV="$(alembic_run_mounted 'heads -v' | awk '/^Rev:/{print $2; exit}')"
|
||||||
|
set -e
|
||||||
|
[[ -n "${HEADREV:-}" ]] || die "Не удалось определить HEAD ревизию"
|
||||||
|
|
||||||
|
# Переписываем alembic_version в файле БД (внутри контейнера сервиса)
|
||||||
|
docker compose run --rm -T \
|
||||||
|
-v "$PWD":/app \
|
||||||
|
-w /app \
|
||||||
|
"${SERVICE}" sh -lc "sqlite3 /app/db/bot.db \"UPDATE alembic_version SET version_num='${HEADREV}';\" || true"
|
||||||
|
|
||||||
|
# Повторяем апгрейд
|
||||||
|
alembic_run_mounted 'upgrade head' || die "Повторный upgrade head не удался"
|
||||||
|
else
|
||||||
|
die "alembic upgrade head не удался"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 3) Сначала выравниваем цепочку миграций ---
|
# --- 3) Выравниваем миграции до текущего HEAD ---
|
||||||
safe_upgrade
|
safe_upgrade
|
||||||
|
|
||||||
# --- 4) (Опционально) создаём новую ревизию с твоим комментарием ---
|
# --- 4) (опционально) создаём ревизию с комментарием и применяем её ---
|
||||||
MIG_MSG="${1-}"
|
MIG_MSG="${1-}"
|
||||||
if [[ -z "${MIG_MSG}" ]]; then
|
if [[ -z "${MIG_MSG}" ]]; then
|
||||||
read -rp "[update.sh] Комментарий для новой миграции (Enter — пропустить): " MIG_MSG || true
|
read -rp "[update.sh] Комментарий для новой миграции (Enter — пропустить): " MIG_MSG || true
|
||||||
@@ -74,9 +106,8 @@ fi
|
|||||||
|
|
||||||
if [[ -n "${MIG_MSG}" ]]; then
|
if [[ -n "${MIG_MSG}" ]]; then
|
||||||
log "Создаю ревизию Alembic с комментарием: ${MIG_MSG}"
|
log "Создаю ревизию Alembic с комментарием: ${MIG_MSG}"
|
||||||
docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic revision --autogenerate -m \"${MIG_MSG}\"" \
|
alembic_run_mounted "revision --autogenerate -m \"${MIG_MSG}\"" || die "alembic revision --autogenerate не удался"
|
||||||
|| die "alembic revision --autogenerate не удался"
|
# Сразу применяем
|
||||||
# применяем новую ревизию
|
|
||||||
safe_upgrade
|
safe_upgrade
|
||||||
else
|
else
|
||||||
log "Создание ревизии пропущено."
|
log "Создание ревизии пропущено."
|
||||||
@@ -98,6 +129,18 @@ docker compose exec -T "${SERVICE}" sh -lc "
|
|||||||
else
|
else
|
||||||
echo 'Внимание: /app/db/bot.db отсутствует!';
|
echo 'Внимание: /app/db/bot.db отсутствует!';
|
||||||
fi
|
fi
|
||||||
"
|
" || true
|
||||||
|
|
||||||
log "Готово ✅"
|
log "Готово ✅"
|
||||||
|
log "Проверка переменных и таблиц внутри контейнера ..."
|
||||||
|
docker compose exec -T "${SERVICE}" sh -lc "
|
||||||
|
echo 'DATABASE_URL='\"\$DATABASE_URL\";
|
||||||
|
cd '${APP_DIR}';
|
||||||
|
echo 'Alembic HEADS:'; alembic heads -v || true;
|
||||||
|
echo 'Alembic CURRENT:'; alembic current -v || true;
|
||||||
|
if [ -f /app/db/bot.db ]; then
|
||||||
|
echo 'Таблицы SQLite (/app/db/bot.db):';
|
||||||
|
sqlite3 /app/db/bot.db '.tables' || true;
|
||||||
|
else
|
||||||
|
echo 'Внимание: /app/db/bot.db отсутствует!';
|
||||||
|
fi
|
||||||
@@ -3,6 +3,9 @@ import hashlib, secrets
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from models import Admin, Channel, ChannelAccess, SCOPE_POST, SCOPE_SHARE
|
from models import Admin, Channel, ChannelAccess, SCOPE_POST, SCOPE_SHARE
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def make_token(nbytes: int = 9) -> str:
|
def make_token(nbytes: int = 9) -> str:
|
||||||
# Короткий URL-safe токен (<= ~12-16 символов укладывается в /start payload)
|
# Короткий URL-safe токен (<= ~12-16 символов укладывается в /start payload)
|
||||||
@@ -41,28 +44,25 @@ async def has_scope_on_channel(session, admin_id: int, channel_id: int, scope: i
|
|||||||
return (acc.scopes & scope) == scope
|
return (acc.scopes & scope) == scope
|
||||||
|
|
||||||
async def list_channels_for_admin(session, admin_id: int):
|
async def list_channels_for_admin(session, admin_id: int):
|
||||||
"""Каналы, куда можно постить: владелец + активные доступы с SCOPE_POST."""
|
|
||||||
# Владелец
|
|
||||||
q1 = await session.execute(select(Channel).where(Channel.admin_id == admin_id))
|
q1 = await session.execute(select(Channel).where(Channel.admin_id == admin_id))
|
||||||
owned = q1.scalars().all()
|
owned = q1.scalars().all()
|
||||||
|
try:
|
||||||
# Доступы
|
|
||||||
q2 = await session.execute(
|
q2 = await session.execute(
|
||||||
select(ChannelAccess).where(
|
select(ChannelAccess).where(
|
||||||
ChannelAccess.invited_admin_id == admin_id,
|
ChannelAccess.invited_admin_id == admin_id,
|
||||||
ChannelAccess.status == "active",
|
ChannelAccess.status == "active",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
access_rows = q2.scalars().all()
|
rows = q2.scalars().all()
|
||||||
access_map = {ar.channel_id for ar in access_rows if (ar.scopes & SCOPE_POST)}
|
except OperationalError:
|
||||||
if not access_map:
|
return owned # таблицы ещё нет — просто вернём свои каналы
|
||||||
|
|
||||||
|
can_post_ids = {r.channel_id for r in rows if (r.scopes & SCOPE_POST)}
|
||||||
|
if not can_post_ids:
|
||||||
return owned
|
return owned
|
||||||
|
q3 = await session.execute(select(Channel).where(Channel.id.in_(can_post_ids)))
|
||||||
q3 = await session.execute(select(Channel).where(Channel.id.in_(access_map)))
|
|
||||||
shared = q3.scalars().all()
|
shared = q3.scalars().all()
|
||||||
|
d = {c.id: c for c in owned}
|
||||||
# Уникальный список (owner + shared)
|
|
||||||
all_channels = {c.id: c for c in owned}
|
|
||||||
for c in shared:
|
for c in shared:
|
||||||
all_channels[c.id] = c
|
d[c.id] = c
|
||||||
return list(all_channels.values())
|
return list(d.values())
|
||||||
|
|||||||
Reference in New Issue
Block a user