Добавлен функционал для работы с S3 хранилищем и обновление контента опубликованных постов
- В `env.example` добавлены настройки для S3 хранилища. - Обновлен файл зависимостей `requirements.txt`, добавлена библиотека `aioboto3` для работы с S3. - В `PostRepository` и `AsyncBotDB` реализованы методы для обновления и получения контента опубликованных постов. - Обновлены обработчики публикации постов для сохранения идентификаторов опубликованных сообщений и их контента. - Реализована логика сохранения медиафайлов в S3 или на локальный диск в зависимости от конфигурации. - Обновлены тесты для проверки нового функционала.
This commit is contained in:
166
scripts/add_published_posts_support.py
Executable file
166
scripts/add_published_posts_support.py
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Скрипт миграции для добавления поддержки опубликованных постов:
|
||||
1. Добавляет колонку published_message_id в таблицу post_from_telegram_suggest
|
||||
2. Создает таблицу published_post_content для хранения медиафайлов опубликованных постов
|
||||
3. Создает индексы для производительности
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from logs.custom_logger import logger
|
||||
|
||||
DEFAULT_DB_PATH = "database/tg-bot-database.db"
|
||||
|
||||
|
||||
def _column_exists(rows: list, name: str) -> bool:
|
||||
"""Проверяет существование колонки в таблице.
|
||||
PRAGMA table_info returns (cid, name, type, notnull, dflt_value, pk)."""
|
||||
for row in rows:
|
||||
if row[1] == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
async def main(db_path: str, dry_run: bool = False) -> None:
|
||||
"""Выполняет миграцию БД для поддержки опубликованных постов."""
|
||||
db_path = os.path.abspath(db_path)
|
||||
if not os.path.exists(db_path):
|
||||
logger.error("База данных не найдена: %s", db_path)
|
||||
print(f"Ошибка: база данных не найдена: {db_path}")
|
||||
return
|
||||
|
||||
async with aiosqlite.connect(db_path) as conn:
|
||||
await conn.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
changes_made = []
|
||||
|
||||
# 1. Проверяем и добавляем колонку published_message_id
|
||||
cursor = await conn.execute(
|
||||
"PRAGMA table_info(post_from_telegram_suggest)"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
|
||||
if not _column_exists(rows, "published_message_id"):
|
||||
if dry_run:
|
||||
print("DRY RUN: Будет добавлена колонка published_message_id в post_from_telegram_suggest")
|
||||
changes_made.append("Добавление колонки published_message_id")
|
||||
else:
|
||||
logger.info("Добавление колонки published_message_id в post_from_telegram_suggest")
|
||||
await conn.execute(
|
||||
"ALTER TABLE post_from_telegram_suggest "
|
||||
"ADD COLUMN published_message_id INTEGER"
|
||||
)
|
||||
await conn.commit()
|
||||
print("✓ Колонка published_message_id добавлена в post_from_telegram_suggest")
|
||||
changes_made.append("Добавлена колонка published_message_id")
|
||||
else:
|
||||
print("✓ Колонка published_message_id уже существует в post_from_telegram_suggest")
|
||||
|
||||
# 2. Проверяем и создаем таблицу published_post_content
|
||||
cursor = await conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='published_post_content'"
|
||||
)
|
||||
table_exists = await cursor.fetchone()
|
||||
await cursor.close()
|
||||
|
||||
if not table_exists:
|
||||
if dry_run:
|
||||
print("DRY RUN: Будет создана таблица published_post_content")
|
||||
changes_made.append("Создание таблицы published_post_content")
|
||||
else:
|
||||
logger.info("Создание таблицы published_post_content")
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS published_post_content (
|
||||
published_message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
content_type TEXT,
|
||||
published_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (published_message_id, content_name)
|
||||
)
|
||||
""")
|
||||
await conn.commit()
|
||||
print("✓ Таблица published_post_content создана")
|
||||
changes_made.append("Создана таблица published_post_content")
|
||||
else:
|
||||
print("✓ Таблица published_post_content уже существует")
|
||||
|
||||
# 3. Проверяем и создаем индексы
|
||||
indexes = [
|
||||
("idx_published_post_content_message_id",
|
||||
"CREATE INDEX IF NOT EXISTS idx_published_post_content_message_id "
|
||||
"ON published_post_content(published_message_id)"),
|
||||
("idx_post_from_telegram_suggest_published",
|
||||
"CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_published "
|
||||
"ON post_from_telegram_suggest(published_message_id)")
|
||||
]
|
||||
|
||||
for index_name, index_sql in indexes:
|
||||
cursor = await conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
||||
(index_name,)
|
||||
)
|
||||
index_exists = await cursor.fetchone()
|
||||
await cursor.close()
|
||||
|
||||
if not index_exists:
|
||||
if dry_run:
|
||||
print(f"DRY RUN: Будет создан индекс {index_name}")
|
||||
changes_made.append(f"Создание индекса {index_name}")
|
||||
else:
|
||||
logger.info(f"Создание индекса {index_name}")
|
||||
await conn.execute(index_sql)
|
||||
await conn.commit()
|
||||
print(f"✓ Индекс {index_name} создан")
|
||||
changes_made.append(f"Создан индекс {index_name}")
|
||||
else:
|
||||
print(f"✓ Индекс {index_name} уже существует")
|
||||
|
||||
# Финальная статистика
|
||||
if dry_run:
|
||||
if changes_made:
|
||||
print("\n" + "="*60)
|
||||
print("DRY RUN: Следующие изменения будут выполнены:")
|
||||
for change in changes_made:
|
||||
print(f" - {change}")
|
||||
print("="*60)
|
||||
else:
|
||||
print("\n✓ Все необходимые изменения уже применены. Ничего делать не нужно.")
|
||||
else:
|
||||
if changes_made:
|
||||
logger.info(f"Миграция завершена. Выполнено изменений: {len(changes_made)}")
|
||||
print(f"\n✓ Миграция завершена успешно!")
|
||||
print(f"Выполнено изменений: {len(changes_made)}")
|
||||
for change in changes_made:
|
||||
print(f" - {change}")
|
||||
else:
|
||||
print("\n✓ Все необходимые изменения уже применены. Ничего делать не нужно.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Добавление поддержки опубликованных постов в БД"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
|
||||
help="Путь к БД (или переменная окружения DB_PATH)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Показать что будет сделано без выполнения изменений",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(args.db, dry_run=args.dry_run))
|
||||
144
scripts/test_s3_connection.py
Executable file
144
scripts/test_s3_connection.py
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Скрипт для проверки подключения к S3 хранилищу.
|
||||
Читает настройки из .env файла или переменных окружения.
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# Загружаем .env файл
|
||||
from dotenv import load_dotenv
|
||||
env_path = os.path.join(project_root, '.env')
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
|
||||
try:
|
||||
import aioboto3
|
||||
except ImportError:
|
||||
print("❌ Библиотека aioboto3 не установлена.")
|
||||
print("Установите её командой: pip install aioboto3")
|
||||
sys.exit(1)
|
||||
|
||||
# Данные для подключения из .env или переменных окружения
|
||||
S3_ACCESS_KEY = os.getenv('S3_ACCESS_KEY', 'j3tears100@gmail.com')
|
||||
S3_SECRET_KEY = os.getenv('S3_SECRET_KEY', 'wQ1-6sZEPs92sbZTSf96')
|
||||
S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', 'https://api.s3.miran.ru:443')
|
||||
S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', 'telegram-helper-bot')
|
||||
S3_REGION = os.getenv('S3_REGION', 'us-east-1')
|
||||
|
||||
async def test_s3_connection():
|
||||
"""Тестирует подключение к S3 хранилищу."""
|
||||
print("🔍 Тестирование подключения к S3 хранилищу...")
|
||||
print(f"Endpoint: {S3_ENDPOINT_URL}")
|
||||
print(f"Bucket: {S3_BUCKET_NAME}")
|
||||
print(f"Region: {S3_REGION}")
|
||||
print(f"Access Key: {S3_ACCESS_KEY}")
|
||||
print()
|
||||
|
||||
session = aioboto3.Session()
|
||||
|
||||
try:
|
||||
async with session.client(
|
||||
's3',
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name=S3_REGION
|
||||
) as s3:
|
||||
# Пытаемся получить список бакетов (может не иметь прав, пропускаем если ошибка)
|
||||
print("📦 Получение списка бакетов...")
|
||||
try:
|
||||
response = await s3.list_buckets()
|
||||
buckets = response.get('Buckets', [])
|
||||
print(f"✅ Подключение успешно! Найдено бакетов: {len(buckets)}")
|
||||
|
||||
if buckets:
|
||||
print("\n📋 Список бакетов:")
|
||||
for bucket in buckets:
|
||||
print(f" - {bucket['Name']} (создан: {bucket.get('CreationDate', 'неизвестно')})")
|
||||
else:
|
||||
print("\n⚠️ Бакеты не найдены.")
|
||||
except Exception as list_error:
|
||||
print(f"⚠️ Не удалось получить список бакетов: {list_error}")
|
||||
print(" Это нормально, если нет прав на list_buckets")
|
||||
print(" Продолжаем тестирование с указанным бакетом...")
|
||||
|
||||
# Пытаемся создать тестовый файл в указанном бакете
|
||||
print("\n🧪 Тестирование записи файла...")
|
||||
# Используем первый найденный бакет, если указанный не найден
|
||||
test_bucket = S3_BUCKET_NAME
|
||||
if buckets:
|
||||
# Проверяем, есть ли указанный бакет в списке
|
||||
bucket_names = [b['Name'] for b in buckets]
|
||||
if test_bucket not in bucket_names:
|
||||
print(f"⚠️ Бакет '{test_bucket}' не найден в списке.")
|
||||
print(f" Используем первый найденный бакет: '{buckets[0]['Name']}'")
|
||||
test_bucket = buckets[0]['Name']
|
||||
|
||||
test_key = 'test-connection.txt'
|
||||
test_content = b'Test connection to S3 storage'
|
||||
|
||||
try:
|
||||
# Проверяем существование бакета
|
||||
try:
|
||||
await s3.head_bucket(Bucket=test_bucket)
|
||||
print(f"✅ Бакет '{test_bucket}' существует и доступен")
|
||||
except Exception as head_error:
|
||||
print(f"❌ Бакет '{test_bucket}' недоступен: {head_error}")
|
||||
print(" Проверьте права доступа к бакету")
|
||||
return False
|
||||
|
||||
await s3.put_object(
|
||||
Bucket=test_bucket,
|
||||
Key=test_key,
|
||||
Body=test_content
|
||||
)
|
||||
print(f"✅ Файл успешно записан в бакет '{test_bucket}' с ключом '{test_key}'")
|
||||
|
||||
# Пытаемся прочитать файл
|
||||
print("🧪 Тестирование чтения файла...")
|
||||
response = await s3.get_object(Bucket=test_bucket, Key=test_key)
|
||||
content = await response['Body'].read()
|
||||
|
||||
if content == test_content:
|
||||
print("✅ Файл успешно прочитан, содержимое совпадает")
|
||||
else:
|
||||
print("⚠️ Файл прочитан, но содержимое не совпадает")
|
||||
|
||||
# Удаляем тестовый файл
|
||||
print("🧹 Удаление тестового файла...")
|
||||
await s3.delete_object(Bucket=test_bucket, Key=test_key)
|
||||
print("✅ Тестовый файл удален")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при тестировании записи/чтения: {e}")
|
||||
print(f" Тип ошибки: {type(e).__name__}")
|
||||
import traceback
|
||||
print(f" Полный traceback:")
|
||||
traceback.print_exc()
|
||||
print("\nВозможные причины:")
|
||||
print(" 1. Неверное имя бакета")
|
||||
print(" 2. Нет прав на запись в бакет")
|
||||
print(" 3. Неверный endpoint URL или регион")
|
||||
print(" 4. Проблемы с форматом endpoint (попробуйте без :443)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка подключения к S3: {e}")
|
||||
print("\nВозможные причины:")
|
||||
print(" 1. Неверные credentials (Access Key / Secret Key)")
|
||||
print(" 2. Неверный endpoint URL")
|
||||
print(" 3. Проблемы с сетью")
|
||||
print(" 4. Неверный регион (попробуйте изменить region_name)")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(test_s3_connection())
|
||||
sys.exit(0 if result else 1)
|
||||
Reference in New Issue
Block a user