# 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 # Проверка режима отладки log_debug = os.getenv('LOG_DEBUG_DATA', 'false').lower() == 'true' changes_made = False # Флаг для отслеживания изменений try: # Подключение без указания базы данных conn = mysql.connector.connect( host=DB_CONFIG['host'], user=DB_CONFIG['user'], password=DB_CONFIG['password'] ) cursor = conn.cursor(buffered=True) # Используем буферизованный курсор # Создание базы данных если не существует # Проверяем существование базы данных cursor.execute(""" SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = %s """, (DB_CONFIG['database'],)) db_exists = cursor.fetchone()[0] > 0 if not db_exists: cursor.execute(f"CREATE DATABASE {DB_CONFIG['database']} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") changes_made = True log_message(f"Database {DB_CONFIG['database']} created") else: if log_debug: log_message(f"Database {DB_CONFIG['database']} already exists") cursor.execute(f"USE {DB_CONFIG['database']}") # Список таблиц и их структур 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), authors VARCHAR(255), 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, tg_id BIGINT NULL, image_url VARCHAR(2048), is_tg_published BOOLEAN DEFAULT 0, tg_published_date DATETIME NULL, marked_for_publication BOOLEAN DEFAULT 0 ) 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) changes_made = True log_message(f"Table {table_name} created") else: if log_debug: 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) ) changes_made = True 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'}, {'column': 'tg_id', 'type': 'BIGINT NULL'}, {'column': 'image_url', 'type': 'VARCHAR(2048)'}, {'column': 'is_tg_published', 'type': 'BOOLEAN DEFAULT 0'}, {'column': 'tg_published_date', 'type': 'DATETIME NULL'}, {'column': 'marked_for_publication', 'type': 'BOOLEAN DEFAULT 0'} ], 'events': [ {'column': 'authors', 'type': 'VARCHAR(255)'} ] } 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}") changes_made = True log_message(f"Added column {column} to table {table_name}") else: if log_debug: 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: # Извлекаем имя индекса и таблицы из SQL index_name = index_sql.split('IF NOT EXISTS')[1].split(' ON ')[0].strip() table_name = index_sql.split(' ON ')[1].split(' (')[0].strip() # Проверяем существование индекса cursor.execute(""" SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = %s AND table_name = %s AND index_name = %s """, (DB_CONFIG['database'], table_name, index_name)) index_exists = cursor.fetchone()[0] > 0 if not index_exists: # Индекс не существует, создаем его cursor.execute(index_sql) changes_made = True log_message(f"Index created: {index_name}") else: if log_debug: log_message(f"Index {index_name} already exists") 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()