Загрузка данных


# ---*--- Начало gui/1admin_db_inter.py ---*---
import os
import shutil
import sqlite3
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
from pathlib import Path

from .admin_db_work import DBFileManager

class AdminDBInter(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent_main = parent
        self.title("Управление базами данных")
        self.geometry("600x400")
        self.resizable(True, True)
        self.transient(parent)
        self.grab_set()
        
        # Здесь тоже используем правильный импорт из пакета administrator
        self.file_manager = DBFileManager() 
        self.build_ui()
        self.refresh_backups_list()
        self.sync_active_flag()
 
        # ВАЖНО: ВСТАВЬ ЭТОТ БЛОК САМЫМ ПОСЛЕДНИМ ДЕЙСТВИЕМ!
        # Центрирование окна над главным приложением Dozor
        self.update_idletasks()

        root_win = self.winfo_toplevel()
        p_x = root_win.winfo_rootx()
        p_y = root_win.winfo_rooty()
        w = self.winfo_width()
        h = self.winfo_height()

        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()

        x = int(p_x + (screen_w - w) / 2)
        y = int(p_y + (screen_h - h) / 2)

        # Устанавливаем позицию окна
        self.geometry(f"+{x}+{y}") 

    def build_ui(self):
        container = ttk.Frame(self)
        container.pack(fill="both", expand=True, padx=10, pady=10)

        top_panel = ttk.Frame(container)
        top_panel.pack(fill="x", pady=10)

        self.use_master_db = tk.BooleanVar(value=(self.file_manager._get_active_name() == "masterdata"))
        style = ttk.Style()
        style.configure("RedText.TCheckbutton", foreground="red")
        
        ttk.Checkbutton(
            top_panel,
            text="Использовать основную базу данных (masterdata.db)",
            variable=self.use_master_db,
            style="RedText.TCheckbutton",
            command=self.toggle_active_db
        ).pack(side="left", padx=10)

        tools_panel = ttk.Frame(container)
        tools_panel.pack(fill="x", pady=10)

        ttk.Button(tools_panel, text="Создать резервную копию", command=self.do_backup).pack(side="left", padx=5)
        ttk.Button(tools_panel, text="Восстановить из копии", command=self.do_restore).pack(side="left", padx=5)
        ttk.Button(tools_panel, text="Мигрировать данные", command=self.do_migrate).pack(side="left", padx=5)

        backup_columns = ("Дата", "Файл")
        self.backups_tree = ttk.Treeview(container, columns=backup_columns, show="headings", height=10)
        for col in backup_columns:
            self.backups_tree.heading(col, text=col)
            self.backups_tree.column(col, width=270)
        self.backups_tree.pack(fill="both", expand=True, pady=10)

    def refresh_backups_list(self):
        self.backups_tree.delete(*self.backups_tree.get_children())
        
        backup_dir = Path(self.file_manager.backup_dir)
        
        if not backup_dir.exists():
            return
            
        # Сортируем по дате создания файла на диске, а не по имени
        files = sorted(backup_dir.glob("*.db"), key=os.path.getctime, reverse=True)
        
        for file_path in files:
            # Выводим только имя файла без лишних преобразований
            display_name = file_path.name 
            # Дата модификации файла в читаемом виде
            mod_time = datetime.fromtimestamp(file_path.stat().st_mtime).strftime("%d.%m %H:%M")
            
            self.backups_tree.insert("", "end", values=(mod_time, display_name))

    def sync_active_flag(self):
        is_master = self.file_manager._get_active_name() == "masterdata"
        self.use_master_db.set(is_master)

    def toggle_active_db(self):
        target = "masterdata" if self.use_master_db.get() else "database"
        self.file_manager._set_active_name(target)
        messagebox.showinfo("Готово", f"Теперь активной будет {target}.db\nПерезапустите приложение для полной смены контекста.")

    def do_backup(self):
        try:
            fname = self.file_manager.create_backup()
            self.refresh_backups_list()
            messagebox.showinfo("Бэкап", f"Копия создана:\n{fname}\nПуть: {self.file_manager.backup_dir}")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))

    def do_restore(self):
        # Игнорируем выделение в таблице. Ищем самый свежий файл сами.
        backup_dir = Path(self.file_manager.backup_dir)
        
        if not backup_dir.exists():
            messagebox.showwarning("Внимание", "Папка с бэкапами пуста.")
            return
            
        files = sorted(backup_dir.glob("*.db"), key=os.path.getctime, reverse=True)
        
        if not files:
            messagebox.showwarning("Внимание", "Не найдено ни одного файла .db в папке backups.")
            return
            
        latest_backup = files[0]
        
        try:
            self.file_manager.restore_from_file(latest_backup.name)
            messagebox.showinfo("Успех", f"База восстановлена из {latest_backup.name}\nПриложение нужно перезапустить.")
        except Exception as e:
            print(f"[CRITICAL RESTORE ERROR] {e}")
            import traceback
            traceback.print_exc() # Выведем полную ошибку в консоль
            messagebox.showerror("Ошибка", f"Критическая ошибка при восстановлении:\n{str(e)}")

    def do_migrate(self):
        answer = messagebox.askyesnocancel(
            "Миграция данных",
            "Перенести ВСЕ данные из НЕАКТИВНОЙ базы в АКТИВНУЮ?\nАктивная база будет ПОЛНОСТЬЮ ЗАТЕРТА данными из другой.\nДействие необратимо."
        )
        if answer is None or not answer:
            return
            
        current = self.file_manager._get_active_name()
        source = "masterdata" if current == "database" else "database"
        try:
            self.file_manager.migrate_data(source, current)
            messagebox.showinfo("Миграция", f"Данные перенесены из {source}.db в {current}.db\nПерезапустите приложение.")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))
# ---*--- Конец файла ---*---















# ---*--- Начало обновленного modules/administrator/1admin_db_work.py ---*---
import io
import os
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path

class DBFileManager:
    def __init__(self):
        # ЕДИНСТВЕННАЯ точка определения корня проекта
        self.project_root = Path(__file__).resolve().parents[2]
        
        self.databases = {
            "database": str(self.project_root / "db" / "database.db"),
            "masterdata": str(self.project_root / "db" / "masterdata.db")
        }
        self.active_flag_path = str(self.project_root / "db" / "active.flag")
        self.backup_dir = str(self.project_root / "backups")  # Теперь точно сюда
        
        os.makedirs(self.backup_dir, exist_ok=True)
        self._ensure_active_flag()

    def _ensure_active_flag(self):
        try:
            if not os.path.exists(self.active_flag_path):
                with open(self.active_flag_path, 'w') as f:
                    f.write("database")
        except Exception as e:
            print(f"[WARN] Не удалось создать флаг активной БД: {e}")

    def _get_active_name(self):
        try:
            with open(self.active_flag_path, 'r') as f:
                name = f.read().strip()
                return name if name in self.databases else "database"
        except FileNotFoundError:
            return "database"

    def _set_active_name(self, name):
        with open(self.active_flag_path, 'w') as f:
            f.write(name)

    def get_active_path(self):
        return self.databases[self._get_active_name()]

    def create_backup(self):
        src = self.get_active_path()
        ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        filename = f"{ts}__{os.path.basename(src)}"
        dst = os.path.join(self.backup_dir, filename)
        shutil.copy2(src, dst)
        return filename

    def restore_from_file(self, backup_filename):
        import io

        project_root = Path(__file__).resolve().parents[2]
        
        src_path = str(project_root / "backups" / backup_filename)
        active_name_str = self._get_active_name()
        dest_path = str(project_root / "db" / f"{active_name_str}.db")

        print(f"[DEBUG RESTORE-DUMP] From Backup: {src_path}")
        print(f"[DEBUG RESTORE-DUMP] To Active DB: {dest_path}")

        try:
            # 1. Читаем бэкап в строку
            with sqlite3.connect(src_path) as src_con:
                sql_dump_io = io.StringIO()
                for line in src_con.iterdump():
                    sql_dump_io.write(line + '\n')
                full_sql_script = sql_dump_io.getvalue()
            
            print("[DEBUG RESTORE-DUMP] Бэкап прочитан в RAM.")

            # === ФИЛЬТРАЦИЯ СКРИПТА (Убираем CREATE TABLE) ===
            lines = full_sql_script.split('\n')
            filtered_lines = []
            skip_create_table = False
            
            for line in lines:
                if line.strip().upper().startswith('CREATE TABLE'):
                    skip_create_table = True
                    continue
                
                if skip_create_table:
                    if ');' in line:
                        skip_create_table = False
                    continue
                
                filtered_lines.append(line)
            
            filtered_script = '\n'.join(filtered_lines)
            # ==================================================================================

            # 2. Выполнение БЕЗ ручного BEGIN/COMMIT 
            with sqlite3.connect(dest_path) as dest_con:
                cursor = dest_con.cursor()
                
                # Выключаем ключи для скорости очистки
                cursor.execute("PRAGMA foreign_keys=OFF;")
                
                # ОЧИСТКА ВСЕХ ТАБЛИЦ ОДНИМ ЗАПРОСОМ (вместо цикла FOR)
                # Это быстрее и надежнее. Сбрасываем счетчики AUTOINCREMENT тоже.
                cursor.execute("""
                    DELETE FROM sqlite_sequence;
                """)
                
                tables_query = """
                    SELECT name FROM sqlite_master 
                    WHERE type='table' AND name NOT LIKE 'sqlite_%';
                """
                cursor.execute(tables_query)
                tables = [row[0] for row in cursor.fetchall()]
                
                delete_statements = "; ".join([f"DELETE FROM {t}" for t in tables]) + ";"
                if delete_statements.strip() != ";":
                    cursor.executescript(delete_statements)

                # Вставка данных. executescript сам управляет своими внутренними транзакциями.
                cursor.executescript(filtered_script)
                
                # Включаем ключи обратно
                cursor.execute("PRAGMA foreign_keys=ON;")
                
            print("[DEBUG RESTORE-DUMP] SUCCESS: Восстановление завершено.")

        except Exception as e:
            # Выведем ПЕРВИЧНУЮ ошибку в консоль, чтобы понять, почему скрипт упал
            print(f"[CRITICAL DUMP ERROR] Первичная ошибка дампа: {str(e)}")
            import traceback
            traceback.print_exc() 
            
            raise RuntimeError(f"Восстановление прервано. Смотрите логи в терминале.")

    def migrate_data(self, source_key: str, dest_key: str):
        import io
        import re

        project_root = Path(__file__).resolve().parents[2]
        source_path = str(project_root / "db" / f"{source_key}.db")
        dest_path = str(project_root / "db" / f"{dest_key}.db")

        print(f"[DEBUG MIGRATE-DUMP] From: {source_path}")
        print(f"[DEBUG MIGRATE-DUMP] To: {dest_path} (через SQL Dump)")

        try:
            # 1. Читаем ВСЮ исходную базу в строку
            with sqlite3.connect(source_path) as src_con:
                sql_dump_io = io.StringIO()
                for line in src_con.iterdump():
                    sql_dump_io.write(line + '\n')
                full_sql_script = sql_dump_io.getvalue()
            
            print("[DEBUG DUMP] Данные успешно сериализованы в SQL.")

            # 2. Фильтруем скрипт: убираем CREATE TABLE, оставляем INSERT и прочее
            lines = full_sql_script.split('\n')
            filtered_lines = []
            
            skip_create_table = False
            for line in lines:
                if line.strip().upper().startswith('CREATE TABLE'):
                    skip_create_table = True
                    continue
                
                if skip_create_table:
                    if ');' in line:
                        skip_create_table = False
                    continue
                
                filtered_lines.append(line)
            
            filtered_script = '\n'.join(filtered_lines)

            # 3. Очищаем целевую базу и вставляем данные БЕЗ ручного BEGIN/COMMIT
            with sqlite3.connect(dest_path) as dest_con:
                cursor = dest_con.cursor()
                
                cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
                tables = [row[0] for row in cursor.fetchall() if not row[0].startswith('sqlite_')]
                
                cursor.execute("PRAGMA foreign_keys=OFF;")
                
                for table in tables:
                    cursor.execute(f"DELETE FROM {table};")
                
                cursor.executescript(filtered_script)
                
                cursor.execute("PRAGMA foreign_keys=ON;")
            
            print("[DEBUG MIGRATE-DUMP] SUCCESS: Данные перенесены через фильтрованный SQL-скрипт.")

        except Exception as e:
            # Сюда упала ошибка SyntaxError - этого блока не хватало
            raise RuntimeError(f"Миграция через SQL Dump прервана: {str(e)}")