326 lines
14 KiB
Python
326 lines
14 KiB
Python
# ensure_db.py
|
|
import mysql.connector
|
|
import os
|
|
import time
|
|
import random
|
|
from datetime import datetime
|
|
from mysql.connector import errorcode
|
|
from dotenv import load_dotenv
|
|
from werkzeug.security import generate_password_hash
|
|
|
|
# Загрузка переменных окружения
|
|
load_dotenv()
|
|
|
|
# Конфигурация из переменных окружения
|
|
DB_CONFIG = {
|
|
'host': os.getenv('MDB_HOST', 'localhost'),
|
|
'user': os.getenv('MDB_USER', 'root'),
|
|
'password': os.getenv('MDB_PW', ''),
|
|
'database': os.getenv('MDBASE', 'vk_posts')
|
|
}
|
|
LOG_FILE = os.getenv('LOG_FILE', 'vk_loader.log')
|
|
LOG_PREFIX = "DB_Ensure"
|
|
|
|
def log_message(message, max_retries=5, retry_delay=0.1):
|
|
"""Функция логирования с повторными попытками"""
|
|
if not LOG_FILE:
|
|
return False
|
|
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
log_line = f"[{timestamp}] [{LOG_PREFIX}] {message}\n"
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
with open(LOG_FILE, 'a', encoding='utf-8') as log:
|
|
log.write(log_line)
|
|
return True
|
|
except (IOError, OSError) as e:
|
|
if attempt < max_retries - 1:
|
|
time.sleep(retry_delay * (1 + random.random()))
|
|
else:
|
|
print(f"Failed to log: {e}")
|
|
return False
|
|
|
|
def ensure_database_structure():
|
|
"""Основная функция проверки и создания БД/таблиц"""
|
|
conn = None
|
|
cursor = None
|
|
|
|
try:
|
|
# Подключение без указания базы данных
|
|
conn = mysql.connector.connect(
|
|
host=DB_CONFIG['host'],
|
|
user=DB_CONFIG['user'],
|
|
password=DB_CONFIG['password']
|
|
)
|
|
cursor = conn.cursor(buffered=True) # Используем буферизованный курсор
|
|
|
|
# Создание базы данных если не существует
|
|
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
|
cursor.execute(f"USE {DB_CONFIG['database']}")
|
|
log_message("Database checked/created successfully")
|
|
|
|
# Список таблиц и их структур
|
|
tables = {
|
|
'posts': '''
|
|
CREATE TABLE posts (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
vk_post_id BIGINT NOT NULL UNIQUE,
|
|
text TEXT,
|
|
image_url VARCHAR(2048),
|
|
vk_post_url VARCHAR(2048),
|
|
published_at DATETIME NULL,
|
|
is_poll BOOLEAN DEFAULT 0,
|
|
poll_question TEXT,
|
|
poll_options TEXT,
|
|
poll_multiple BOOLEAN DEFAULT 0,
|
|
poll_end_date DATETIME NULL,
|
|
marked_for_publication BOOLEAN DEFAULT 0,
|
|
published_in_tg BOOLEAN DEFAULT 0,
|
|
tg_publication_date DATETIME NULL,
|
|
tg_message_id BIGINT NULL,
|
|
tg_poll_id BIGINT NULL,
|
|
tg_markpost_id BIGINT NULL,
|
|
tg_poll_results TEXT DEFAULT '',
|
|
is_event BOOLEAN DEFAULT 0,
|
|
shortname VARCHAR(100),
|
|
action_number BIGINT NULL,
|
|
auto_unit VARCHAR(255)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
''',
|
|
'marks': '''
|
|
CREATE TABLE marks (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
tg_post_id BIGINT NOT NULL,
|
|
tg_user_id BIGINT NOT NULL,
|
|
tg_user_name VARCHAR(255),
|
|
autolink BOOLEAN DEFAULT 0,
|
|
reminded_at DATETIME NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
''',
|
|
'marks_evt': '''
|
|
CREATE TABLE marks_evt (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
tg_event_id BIGINT NOT NULL,
|
|
tg_user_id BIGINT NOT NULL,
|
|
tg_user_name VARCHAR(255),
|
|
autolink BOOLEAN DEFAULT 0,
|
|
reminded_at DATETIME NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
''',
|
|
'users': '''
|
|
CREATE TABLE users (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
username VARCHAR(255) UNIQUE NOT NULL,
|
|
password VARCHAR(255) NOT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
''',
|
|
'events': '''
|
|
CREATE TABLE events (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
id_event BIGINT NOT NULL,
|
|
number BIGINT NOT NULL,
|
|
unit_name VARCHAR(255),
|
|
name VARCHAR(255),
|
|
about VARCHAR(4096),
|
|
about_social_picture VARCHAR(2048),
|
|
modified DATETIME NULL,
|
|
added DATETIME NULL,
|
|
tags VARCHAR(255),
|
|
is_canceled BOOLEAN DEFAULT 0,
|
|
accepted BOOLEAN DEFAULT 0,
|
|
denied BOOLEAN DEFAULT 0,
|
|
is_visible BOOLEAN DEFAULT 0,
|
|
marked_to_publication BOOLEAN DEFAULT 0,
|
|
is_posted_tg BOOLEAN DEFAULT 0,
|
|
tg_message_id BIGINT NULL,
|
|
tg_info_id BIGINT NULL,
|
|
tg_posted_date DATETIME NULL,
|
|
announcement_link BIGINT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
''',
|
|
'interlinks': '''
|
|
CREATE TABLE interlinks (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
post_id INT NOT NULL,
|
|
event_id INT NOT NULL,
|
|
created_at DATETIME NULL,
|
|
auto_added BOOLEAN DEFAULT 0,
|
|
FOREIGN KEY (post_id)
|
|
REFERENCES posts(id)
|
|
ON DELETE CASCADE,
|
|
FOREIGN KEY (event_id)
|
|
REFERENCES events(id)
|
|
ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
''',
|
|
'categories': '''
|
|
CREATE TABLE categories (
|
|
AUTO_ID INT AUTO_INCREMENT PRIMARY KEY,
|
|
ID VARCHAR(255),
|
|
TITLE VARCHAR(2048),
|
|
description TEXT
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
'''
|
|
}
|
|
|
|
# Проверка и создание таблиц
|
|
for table_name, table_sql in tables.items():
|
|
try:
|
|
# Проверяем существование таблицы через information_schema
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM information_schema.tables
|
|
WHERE table_schema = %s AND table_name = %s
|
|
""", (DB_CONFIG['database'], table_name))
|
|
|
|
if cursor.fetchone()[0] == 0:
|
|
# Таблица не существует, создаем её
|
|
cursor.execute(table_sql)
|
|
log_message(f"Table {table_name} created")
|
|
else:
|
|
log_message(f"Table {table_name} exists")
|
|
|
|
except mysql.connector.Error as err:
|
|
log_message(f"Error checking/creating table {table_name}: {err}")
|
|
raise err
|
|
|
|
# Создание дефолтного админа, если таблица users пуста
|
|
try:
|
|
cursor.execute("SELECT COUNT(*) as count FROM users")
|
|
user_count = cursor.fetchone()[0]
|
|
if user_count == 0:
|
|
username = os.getenv('DEFNM')
|
|
password = os.getenv('DEFPW')
|
|
if username and password:
|
|
hashed_password = generate_password_hash(password)
|
|
cursor.execute(
|
|
"INSERT INTO users (username, password) VALUES (%s, %s)",
|
|
(username, hashed_password)
|
|
)
|
|
log_message(f"Created default admin user: {username}")
|
|
else:
|
|
log_message("Environment variables DEFNM/DEFPW not set. Admin not created.")
|
|
except mysql.connector.Error as err:
|
|
log_message(f"Error creating default admin: {err}")
|
|
|
|
# Проверка и добавление новых полей в существующие таблицы
|
|
table_alterations = {
|
|
'posts': [
|
|
{'column': 'auto_unit', 'type': 'VARCHAR(255)'}
|
|
],
|
|
'marks': [
|
|
{'column': 'autolink', 'type': 'BOOLEAN DEFAULT 0'},
|
|
{'column': 'reminded_at', 'type': 'DATETIME NULL'}
|
|
],
|
|
'marks_evt': [
|
|
{'column': 'autolink', 'type': 'BOOLEAN DEFAULT 0'},
|
|
{'column': 'reminded_at', 'type': 'DATETIME NULL'}
|
|
],
|
|
'interlinks': [
|
|
{'column': 'auto_added', 'type': 'BOOLEAN DEFAULT 0'}
|
|
],
|
|
'categories': [
|
|
{'column': 'description', 'type': 'TEXT'}
|
|
]
|
|
}
|
|
|
|
for table_name, alterations in table_alterations.items():
|
|
for alteration in alterations:
|
|
column = alteration['column']
|
|
column_type = alteration['type']
|
|
|
|
try:
|
|
# Проверяем существование столбца
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM information_schema.columns
|
|
WHERE table_schema = %s AND table_name = %s AND column_name = %s
|
|
""", (DB_CONFIG['database'], table_name, column))
|
|
|
|
if cursor.fetchone()[0] == 0:
|
|
# Столбец не существует, добавляем его
|
|
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column} {column_type}")
|
|
log_message(f"Added column {column} to table {table_name}")
|
|
else:
|
|
log_message(f"Column {column} already exists in table {table_name}")
|
|
|
|
except mysql.connector.Error as err:
|
|
log_message(f"Error checking/adding column {column} to table {table_name}: {err}")
|
|
|
|
# Создание индексов для таблицы marks
|
|
marks_indexes = [
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS uniq_post_user ON marks (tg_post_id, tg_user_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_post_subscribers ON marks (tg_post_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_user_subscriptions ON marks (tg_user_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_created_at ON marks (created_at)',
|
|
'CREATE INDEX IF NOT EXISTS idx_post_created ON marks (tg_post_id, created_at)',
|
|
'CREATE INDEX IF NOT EXISTS idx_tg_post_id ON marks (tg_post_id)'
|
|
]
|
|
|
|
# Создание индексов для таблицы marks_evt
|
|
marks_evt_indexes = [
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS uniq_event_user ON marks_evt (tg_event_id, tg_user_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_event_subscribers ON marks_evt (tg_event_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_user_event_subscriptions ON marks_evt (tg_user_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_marks_evt_created_at ON marks_evt (created_at)',
|
|
'CREATE INDEX IF NOT EXISTS idx_event_created ON marks_evt (tg_event_id, created_at)',
|
|
'CREATE INDEX IF NOT EXISTS idx_tg_event_id ON marks_evt (tg_event_id)'
|
|
]
|
|
|
|
# Создание индексов для таблицы events
|
|
events_indexes = [
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_events_id_event ON events (id_event)',
|
|
'CREATE INDEX IF NOT EXISTS idx_events_number ON events (number)',
|
|
'CREATE INDEX IF NOT EXISTS idx_events_is_visible ON events (is_visible)',
|
|
'CREATE INDEX IF NOT EXISTS idx_events_is_posted_tg ON events (is_posted_tg)',
|
|
]
|
|
|
|
# Создание индексов для таблицы interlinks
|
|
interlinks_indexes = [
|
|
'CREATE INDEX IF NOT EXISTS idx_interlinks_post_id ON interlinks (post_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_interlinks_event_id ON interlinks (event_id)',
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS uniq_post_event ON interlinks (post_id, event_id)'
|
|
]
|
|
|
|
# Создание индексов для таблицы categories
|
|
categories_indexes = [
|
|
'CREATE INDEX IF NOT EXISTS idx_categories_id ON categories (ID)'
|
|
]
|
|
|
|
# Объединяем все индексы
|
|
all_indexes = marks_indexes + marks_evt_indexes + events_indexes + interlinks_indexes + categories_indexes
|
|
|
|
for index_sql in all_indexes:
|
|
try:
|
|
cursor.execute(index_sql)
|
|
index_name = index_sql.split('IF NOT EXISTS')[1].split(' ON ')[0].strip()
|
|
log_message(f"Index created: {index_name}")
|
|
except mysql.connector.Error as err:
|
|
log_message(f"Error creating index: {err}")
|
|
|
|
conn.commit()
|
|
log_message("Database structure ensured successfully")
|
|
|
|
except mysql.connector.Error as err:
|
|
log_message(f"Database error: {err}")
|
|
raise
|
|
finally:
|
|
# Аккуратно закрываем соединение и курсор
|
|
try:
|
|
if cursor:
|
|
cursor.close()
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
if conn and conn.is_connected():
|
|
conn.close()
|
|
except:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
ensure_database_structure()
|