diff --git a/.gitignore b/.gitignore index 147c64c..c5b8e85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ __pycache__/ build/ dist/ -*.spec diff --git a/1C_Base_Adder.spec b/1C_Base_Adder.spec new file mode 100644 index 0000000..e74a4ef --- /dev/null +++ b/1C_Base_Adder.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['Main_v3.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='1C_Base_Adder', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['download.ico'], +) diff --git a/Main_v3.py b/Main_v3.py index c3b24eb..437fcf4 100644 --- a/Main_v3.py +++ b/Main_v3.py @@ -1,6 +1,7 @@ import os +import re import tkinter as tk -from tkinter import messagebox +from tkinter import messagebox, filedialog import pyodbc import uuid import sys @@ -71,97 +72,154 @@ if getattr(sys, "stdout", None): class RemoteFolderSelector(tk.Tk): def __init__(self): super().__init__() - self.title("Remote Folder and SQL Database Selector") - self.geometry("620x560") + self.title("Добавление баз 1С пользователям") + self.geometry("760x780") + self.minsize(700, 640) + + # --- Состояние --- + self.ip_address = None # None/"" => локальный ПК, иначе IP удалённого ПК + self.active_user = None # пользователь, чьи базы показаны в панели "Подключённые базы" + self.user_vars = {} # имя пользователя -> BooleanVar (галочка) + self.db_vars = {} # имя базы из SQL -> BooleanVar (галочка) + self.conn_vars = {} # имя подключённой базы активного пользователя -> BooleanVar + + # Компоновка окна: две колонки сверху, широкая кнопка "Добавить" снизу + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) - # Создадим фреймы для лучшей компоновки элементов self.frame_left = tk.Frame(self) self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") self.frame_right = tk.Frame(self) self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") - # Сделаем окна масштабируемыми - self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=1) - self.grid_rowconfigure(0, weight=1) + self._build_left_panel() + self._build_right_panel() - # Текущий режим подключения (локально либо по IP) - self.ip_address = None + # Большая кнопка добавления снизу, на всю ширину окна + self.add_button = tk.Button( + self, text="Добавить отмеченные базы отмеченным пользователям", + font=("Segoe UI", 11, "bold"), command=self.add_database_to_ibases + ) + self.add_button.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 12), sticky="ew") - # Поле ввода IP-адреса - self.ip_label = tk.Label(self.frame_left, text="Enter IP (optional):") - self.ip_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") - - self.ip_entry = tk.Entry(self.frame_left, width=30) - self.ip_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w") - - self.connect_button = tk.Button(self.frame_left, text="Load Users", command=self.load_users) - self.connect_button.grid(row=2, column=0, padx=5, pady=5) - - # Панель "отметить все / снять все" для пользователей - self.user_toolbar = tk.Frame(self.frame_left) - self.user_toolbar.grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w") - tk.Button(self.user_toolbar, text="Отметить все", - command=lambda: self._set_all(self.user_vars, True)).pack(side="left", padx=(0, 4)) - tk.Button(self.user_toolbar, text="Снять все", - command=lambda: self._set_all(self.user_vars, False)).pack(side="left") - - # Список пользователей с чекбоксами - self.user_vars = {} - self.user_container, self.user_inner = self._create_scrollable_checklist(self.frame_left) - self.user_container.grid(row=4, column=0, padx=5, pady=5, sticky="nsew") - self.frame_left.grid_rowconfigure(4, weight=1) - self.frame_left.grid_columnconfigure(0, weight=1) - - # Поля для ввода данных SQL - self.server_label = tk.Label(self.frame_right, text="SQL Server:") - self.server_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") - - self.server_entry = tk.Entry(self.frame_right, width=30) - self.server_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w") - self.server_entry.insert(0, "SERVERSQL") - - self.login_label = tk.Label(self.frame_right, text="SQL Login:") - self.login_label.grid(row=2, column=0, padx=5, pady=5, sticky="w") - - self.login_entry = tk.Entry(self.frame_right, width=30) - self.login_entry.grid(row=3, column=0, padx=5, pady=5, sticky="w") - self.login_entry.insert(0, "sa") - - self.password_label = tk.Label(self.frame_right, text="SQL Password:") - self.password_label.grid(row=4, column=0, padx=5, pady=5, sticky="w") - - self.password_entry = tk.Entry(self.frame_right, show="*", width=30) - self.password_entry.grid(row=5, column=0, padx=5, pady=5, sticky="w") - - self.sql_button = tk.Button(self.frame_right, text="Load Databases", command=self.load_databases) - self.sql_button.grid(row=6, column=0, padx=5, pady=5) - - # Панель "отметить все / снять все" для баз - self.db_toolbar = tk.Frame(self.frame_right) - self.db_toolbar.grid(row=7, column=0, padx=5, pady=(5, 0), sticky="w") - tk.Button(self.db_toolbar, text="Отметить все", - command=lambda: self._set_all(self.db_vars, True)).pack(side="left", padx=(0, 4)) - tk.Button(self.db_toolbar, text="Снять все", - command=lambda: self._set_all(self.db_vars, False)).pack(side="left") - - # Список баз данных с чекбоксами - self.db_vars = {} - self.db_container, self.db_inner = self._create_scrollable_checklist(self.frame_right) - self.db_container.grid(row=8, column=0, padx=5, pady=5, sticky="nsew") - self.frame_right.grid_rowconfigure(8, weight=1) - self.frame_right.grid_columnconfigure(0, weight=1) - - # Кнопка добавления базы - self.add_button = tk.Button(self.frame_left, text="Добавить отмеченные базы", command=self.add_database_to_ibases) - self.add_button.grid(row=5, column=0, padx=5, pady=5) - - # Загрузка пользователей с локального компьютера по умолчанию + # По умолчанию грузим пользователей локального ПК self.load_users() - def _create_scrollable_checklist(self, parent, width=200, height=180): - """Создаём прокручиваемую область для чекбоксов. Возвращаем (container, inner).""" + # ------------------------------------------------------------------ UI + + def _build_left_panel(self): + f = self.frame_left + f.grid_columnconfigure(0, weight=1) + + tk.Label(f, text="IP-адрес (необязательно):").grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w") + self.ip_entry = tk.Entry(f) + self.ip_entry.grid(row=1, column=0, padx=5, pady=(0, 5), sticky="ew") + + tk.Button(f, text="Загрузить пользователей", command=self.load_users)\ + .grid(row=2, column=0, padx=5, pady=5, sticky="ew") + + # Поиск по пользователям + tk.Label(f, text="Поиск пользователей:").grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w") + self.user_search_var = tk.StringVar() + self.user_search_var.trace_add("write", lambda *a: self._render_checklist( + self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click)) + tk.Entry(f, textvariable=self.user_search_var)\ + .grid(row=4, column=0, padx=5, pady=(0, 5), sticky="ew") + + # Отметить все / снять все (с учётом фильтра) + bar = tk.Frame(f) + bar.grid(row=5, column=0, padx=5, pady=(0, 3), sticky="w") + tk.Button(bar, text="Отметить все", + command=lambda: self._set_all(self.user_vars, self.user_search_var.get(), True))\ + .pack(side="left", padx=(0, 4)) + tk.Button(bar, text="Снять все", + command=lambda: self._set_all(self.user_vars, self.user_search_var.get(), False))\ + .pack(side="left") + + # Список пользователей + self.user_container, self.user_inner = self._create_scrollable_checklist(f, height=150) + self.user_container.grid(row=6, column=0, padx=5, pady=5, sticky="nsew") + f.grid_rowconfigure(6, weight=1) + + # Панель подключённых баз активного пользователя + self.conn_label = tk.Label(f, text="Подключённые базы: (выберите пользователя)") + self.conn_label.grid(row=7, column=0, padx=5, pady=(8, 0), sticky="w") + + bar2 = tk.Frame(f) + bar2.grid(row=8, column=0, padx=5, pady=(0, 3), sticky="w") + tk.Button(bar2, text="Отметить все", + command=lambda: self._set_all(self.conn_vars, "", True)).pack(side="left", padx=(0, 4)) + tk.Button(bar2, text="Снять все", + command=lambda: self._set_all(self.conn_vars, "", False)).pack(side="left") + + self.conn_container, self.conn_inner = self._create_scrollable_checklist(f, height=150) + self.conn_container.grid(row=9, column=0, padx=5, pady=5, sticky="nsew") + f.grid_rowconfigure(9, weight=1) + + tk.Button(f, text="Удалить отмеченные базы у пользователя", command=self.delete_connected_bases)\ + .grid(row=10, column=0, padx=5, pady=5, sticky="ew") + + # Опция дублирования в ZIP (roaming) профиль + self.zip_enabled = tk.BooleanVar(value=False) + tk.Checkbutton(f, text="Дублировать изменения в ZIP-профиль пользователя", + variable=self.zip_enabled, command=self._toggle_zip)\ + .grid(row=11, column=0, padx=5, pady=(8, 0), sticky="w") + + zf = tk.Frame(f) + zf.grid(row=12, column=0, padx=5, pady=(0, 5), sticky="ew") + zf.grid_columnconfigure(0, weight=1) + self.zip_path_var = tk.StringVar(value=r"Z:\ZIP_User_Profiles") + self.zip_path_entry = tk.Entry(zf, textvariable=self.zip_path_var, state="disabled") + self.zip_path_entry.grid(row=0, column=0, sticky="ew", padx=(0, 4)) + self.zip_browse_btn = tk.Button(zf, text="Обзор...", state="disabled", command=self._browse_zip) + self.zip_browse_btn.grid(row=0, column=1) + + def _build_right_panel(self): + f = self.frame_right + f.grid_columnconfigure(0, weight=1) + + tk.Label(f, text="SQL-сервер:").grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w") + self.server_entry = tk.Entry(f) + self.server_entry.grid(row=1, column=0, padx=5, pady=(0, 5), sticky="ew") + self.server_entry.insert(0, "SERVERSQL") + + tk.Label(f, text="Логин SQL:").grid(row=2, column=0, padx=5, pady=(5, 0), sticky="w") + self.login_entry = tk.Entry(f) + self.login_entry.grid(row=3, column=0, padx=5, pady=(0, 5), sticky="ew") + self.login_entry.insert(0, "sa") + + tk.Label(f, text="Пароль SQL:").grid(row=4, column=0, padx=5, pady=(5, 0), sticky="w") + self.password_entry = tk.Entry(f, show="*") + self.password_entry.grid(row=5, column=0, padx=5, pady=(0, 5), sticky="ew") + + tk.Button(f, text="Загрузить базы", command=self.load_databases)\ + .grid(row=6, column=0, padx=5, pady=5, sticky="ew") + + # Поиск по базам + tk.Label(f, text="Поиск баз:").grid(row=7, column=0, padx=5, pady=(5, 0), sticky="w") + self.db_search_var = tk.StringVar() + self.db_search_var.trace_add("write", lambda *a: self._render_checklist( + self.db_inner, self.db_vars, self.db_search_var.get())) + tk.Entry(f, textvariable=self.db_search_var)\ + .grid(row=8, column=0, padx=5, pady=(0, 5), sticky="ew") + + bar = tk.Frame(f) + bar.grid(row=9, column=0, padx=5, pady=(0, 3), sticky="w") + tk.Button(bar, text="Отметить все", + command=lambda: self._set_all(self.db_vars, self.db_search_var.get(), True))\ + .pack(side="left", padx=(0, 4)) + tk.Button(bar, text="Снять все", + command=lambda: self._set_all(self.db_vars, self.db_search_var.get(), False))\ + .pack(side="left") + + self.db_container, self.db_inner = self._create_scrollable_checklist(f, height=380) + self.db_container.grid(row=10, column=0, padx=5, pady=5, sticky="nsew") + f.grid_rowconfigure(10, weight=1) + + def _create_scrollable_checklist(self, parent, width=220, height=180): + """Прокручиваемая область для чекбоксов. Возвращает (container, inner).""" container = tk.Frame(parent, borderwidth=1, relief="sunken") canvas = tk.Canvas(container, width=width, height=height, highlightthickness=0) scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview) @@ -172,7 +230,6 @@ class RemoteFolderSelector(tk.Tk): canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") - # Прокрутка колесом мыши, когда курсор над областью def _on_mousewheel(event): canvas.yview_scroll(int(-event.delta / 120), "units") @@ -180,88 +237,189 @@ class RemoteFolderSelector(tk.Tk): canvas.bind("", lambda e: canvas.unbind_all("")) return container, inner - def _populate_checklist(self, inner, vars_dict, items): - """Пересоздаём чекбоксы в области inner по списку items.""" - for child in inner.winfo_children(): - child.destroy() + def _load_items(self, vars_dict, items): + """Создаём свежий набор BooleanVar по списку items (галочки сбрасываются).""" vars_dict.clear() for item in items: - var = tk.BooleanVar(value=False) - vars_dict[item] = var - tk.Checkbutton(inner, text=item, variable=var, anchor="w").pack(fill="x", anchor="w") + vars_dict[item] = tk.BooleanVar(value=False) - def _set_all(self, vars_dict, value): - """Отмечаем (True) либо снимаем (False) все чекбоксы.""" - for var in vars_dict.values(): + def _render_checklist(self, inner, vars_dict, filter_text="", on_click=None): + """Рисуем чекбоксы, показывая только элементы, подходящие под фильтр. + Переменные (галочки) сохраняются между перерисовками — состояние не теряется.""" + for child in inner.winfo_children(): + child.destroy() + ft = (filter_text or "").lower() + for name, var in vars_dict.items(): + if ft and ft not in name.lower(): + continue + cmd = (lambda n=name: on_click(n)) if on_click else None + tk.Checkbutton(inner, text=name, variable=var, anchor="w", command=cmd)\ + .pack(fill="x", anchor="w") + # После перерисовки (например, при вводе в поиск) возвращаем прокрутку наверх, + # иначе список остаётся проскроллен вниз и результат поиска не виден. + inner.master.yview_moveto(0) + + def _set_all(self, vars_dict, filter_text, value): + """Отмечаем/снимаем все чекбоксы, попадающие под текущий фильтр.""" + ft = (filter_text or "").lower() + for name, var in vars_dict.items(): + if ft and ft not in name.lower(): + continue var.set(value) def _get_checked(self, vars_dict): - """Возвращаем список отмеченных элементов.""" return [name for name, var in vars_dict.items() if var.get()] + def _toggle_zip(self): + state = "normal" if self.zip_enabled.get() else "disabled" + self.zip_path_entry.config(state=state) + self.zip_browse_btn.config(state=state) + + def _browse_zip(self): + path = filedialog.askdirectory(title="Каталог с ZIP-профилями пользователей") + if path: + self.zip_path_var.set(os.path.normpath(path)) + + # -------------------------------------------------------------- Загрузка + def load_users(self): - """Загружаем пользователей с указанного IP-адреса или локального ПК.""" - self.ip_address = self.ip_entry.get() - - if not self.ip_address: # Если IP не указан, используем локальный ПК - local_path = r"C:\Users" - self.load_local_users(local_path) + """Загружаем список пользователей с локального ПК или с удалённого по IP.""" + self.ip_address = self.ip_entry.get().strip() + if not self.ip_address: + base = r"C:\Users" else: - try: - # Используем указанный IP для подключения к удалённому ПК - remote_path = f"\\\\{self.ip_address}\\C$\\Users" - self.load_remote_users(remote_path) - except Exception as e: - messagebox.showerror("Error", f"Could not load users: {str(e)}") - - def load_local_users(self, path): - """Загружаем список пользователей с локального ПК.""" + base = f"\\\\{self.ip_address}\\C$\\Users" try: - folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))] - self._populate_checklist(self.user_inner, self.user_vars, folders) + folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f))] except Exception as e: - messagebox.showerror("Error", f"Could not load local users: {str(e)}") + messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}") + return - def load_remote_users(self, remote_path): - """Загружаем список пользователей с удалённого ПК.""" - try: - folders = [f for f in os.listdir(remote_path) if os.path.isdir(os.path.join(remote_path, f))] - self._populate_checklist(self.user_inner, self.user_vars, folders) - except Exception as e: - messagebox.showerror("Error", f"Could not load remote users: {str(e)}") + self._load_items(self.user_vars, folders) + self._render_checklist(self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click) + + # Сбрасываем панель подключённых баз — она относилась к прежнему источнику + self.active_user = None + self.conn_vars.clear() + self._render_checklist(self.conn_inner, self.conn_vars, "") + self.conn_label.config(text="Подключённые базы: (выберите пользователя)") def load_databases(self): - """Подключаемся к SQL серверу и загружаем список баз данных.""" - server = self.server_entry.get() + """Подключаемся к SQL-серверу и загружаем список баз.""" + server = self.server_entry.get().strip() login = self.login_entry.get() password = self.password_entry.get() - try: connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};" connection = pyodbc.connect(connection_string) cursor = connection.cursor() - cursor.execute("SELECT name FROM sys.databases") databases = cursor.fetchall() - - # Скрываем служебные базы SQL Server, они не являются базами 1С - system_dbs = {"master", "tempdb", "model", "msdb"} - names = [db[0] for db in databases if db[0].lower() not in system_dbs] - self._populate_checklist(self.db_inner, self.db_vars, names) - cursor.close() connection.close() except Exception as e: - messagebox.showerror("Error", f"Could not connect to SQL server: {str(e)}") + messagebox.showerror("Ошибка", f"Не удалось подключиться к SQL-серверу:\n{e}") + return + + # Служебные базы SQL Server скрываем — это не базы 1С + system_dbs = {"master", "tempdb", "model", "msdb"} + names = [db[0] for db in databases if db[0].lower() not in system_dbs] + self._load_items(self.db_vars, names) + self._render_checklist(self.db_inner, self.db_vars, self.db_search_var.get()) + + def on_user_click(self, user): + """Клик по пользователю — делаем его активным и показываем его подключённые базы.""" + self.load_connected_bases(user) + + def load_connected_bases(self, user): + """Читаем ibases.v8i пользователя и показываем список его баз в панели.""" + self.active_user = user + self.ip_address = self.ip_entry.get().strip() + path = self.get_ibases_path(user) + text = self._read_ibases(path) + _, sections = self._parse_ibases(text) + names = [name for name, _ in sections] + self._load_items(self.conn_vars, names) + self._render_checklist(self.conn_inner, self.conn_vars, "") + suffix = "" if names else " (баз нет)" + self.conn_label.config(text=f"Подключённые базы: {user}{suffix}") + + # --------------------------------------------------------------- Пути def get_ibases_path(self, user): - """Возвращаем путь к ibases.v8i выбранного пользователя (локально либо по IP).""" + """Путь к ibases.v8i пользователя (локально либо по IP).""" if not self.ip_address: base_path = f"C:\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart" else: base_path = f"\\\\{self.ip_address}\\C$\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart" return os.path.join(base_path, "ibases.v8i") + def get_zip_ibases_path(self, user): + """Путь к ibases.v8i внутри ZIP-профиля пользователя. + Папку профиля ищем автопоиском: либо .* (например .V6). + Возвращаем None, если корень недоступен или профиль не найден.""" + root = self.zip_path_var.get().strip() + if not root or not os.path.isdir(root): + return None + u = user.lower() + candidates = [] + try: + for name in os.listdir(root): + if not os.path.isdir(os.path.join(root, name)): + continue + low = name.lower() + if low == u or low.startswith(u + "."): + candidates.append(name) + except Exception: + return None + if not candidates: + return None + + # Приоритет: точное .V6, затем любой суффикс .*, затем совпадение без суффикса + def score(n): + low = n.lower() + if low == u + ".v6": + return 0 + if "." in low: + return 1 + return 2 + candidates.sort(key=score) + chosen = candidates[0] + return os.path.join(root, chosen, "AppData", "Roaming", "1C", "1CEStart", "ibases.v8i") + + # ------------------------------------------------------- Чтение/запись v8i + + def _read_ibases(self, path): + """Читаем ibases.v8i, устойчиво к кодировке (UTF-8 с BOM / без / cp1251).""" + if not os.path.exists(path): + return "" + for enc in ("utf-8-sig", "cp1251"): + try: + with open(path, "r", encoding=enc) as f: + return f.read() + except UnicodeDecodeError: + continue + with open(path, "r", encoding="utf-8", errors="ignore") as f: + return f.read() + + def _parse_ibases(self, text): + """Разбираем содержимое на (преамбула, [(имя_базы, блок_текста), ...]). + Секция начинается со строки вида [ИмяБазы].""" + header_re = re.compile(r"^\s*\[(.+?)\]\s*$") + preamble = [] + sections = [] # список [имя, [строки]] + current = None + for line in text.splitlines(keepends=True): + m = header_re.match(line) + if m: + current = [m.group(1), [line]] + sections.append(current) + elif current is None: + preamble.append(line) + else: + current[1].append(line) + return "".join(preamble), [(name, "".join(ls)) for name, ls in sections] + def build_base_entry(self, base, server): """Формируем запись одной базы для ibases.v8i.""" return ( @@ -280,74 +438,142 @@ class RemoteFolderSelector(tk.Tk): "DisableLocalSpeechToText=0\n" ) - def add_database_to_ibases(self): - """Добавляем все выбранные базы всем выбранным пользователям.""" - # Актуализируем режим (локально / по IP) на момент добавления - self.ip_address = self.ip_entry.get().strip() + def _add_entries_to_file(self, ibases_path, bases, server): + """Добавляем базы в один файл ibases.v8i (с фильтром дублей). + Возвращаем (добавлено, пропущено).""" + base_dir = os.path.dirname(ibases_path) + if not os.path.exists(base_dir): + os.makedirs(base_dir) + existing = self._read_ibases(ibases_path) + entries = [] + added = skipped = 0 + for base in bases: + connect_line = f'Connect=Srvr="{server}";Ref="{base}";' + if connect_line in existing or any(connect_line in e for e in entries): + skipped += 1 + continue + entries.append(self.build_base_entry(base, server)) + added += 1 + if entries: + with open(ibases_path, "a", encoding="utf-8") as f: + f.write("".join(entries)) + return added, skipped + + def _delete_entries_from_file(self, ibases_path, base_names): + """Удаляем указанные базы из одного файла ibases.v8i. Возвращаем число удалённых.""" + if not os.path.exists(ibases_path): + return 0 + text = self._read_ibases(ibases_path) + preamble, sections = self._parse_ibases(text) + target = {b.lower() for b in base_names} + kept = [blk for (name, blk) in sections if name.lower() not in target] + removed = len(sections) - len(kept) + if removed: + with open(ibases_path, "w", encoding="utf-8") as f: + f.write(preamble + "".join(kept)) + return removed + + # ----------------------------------------------------------- Действия + + def add_database_to_ibases(self): + """Добавляем отмеченные базы всем отмеченным пользователям (+ ZIP по галочке).""" + self.ip_address = self.ip_entry.get().strip() users = self._get_checked(self.user_vars) bases = self._get_checked(self.db_vars) server = self.server_entry.get().strip() if not users: - messagebox.showerror("Error", "Не отмечен ни один пользователь!") + messagebox.showerror("Ошибка", "Не отмечен ни один пользователь!") return if not bases: - messagebox.showerror("Error", "Не отмечена ни одна база!") + messagebox.showerror("Ошибка", "Не отмечена ни одна база!") return if not server: - messagebox.showerror("Error", "Не указан SQL-сервер!") + messagebox.showerror("Ошибка", "Не указан SQL-сервер!") return - added = 0 - skipped = 0 + use_zip = self.zip_enabled.get() + added = skipped = zadded = zskipped = 0 errors = [] for user in users: try: - ibases_path = self.get_ibases_path(user) - base_dir = os.path.dirname(ibases_path) - - # Создаём каталог, если его нет - if not os.path.exists(base_dir): - os.makedirs(base_dir) - - # Читаем текущее содержимое файла для проверки дублей - existing = "" - if os.path.exists(ibases_path): - with open(ibases_path, "r", encoding="utf-8") as f: - existing = f.read() - - entries_to_add = [] - for base in bases: - # Дубль определяем по строке подключения (сервер + имя базы) - connect_line = f'Connect=Srvr="{server}";Ref="{base}";' - already_present = connect_line in existing or any( - connect_line in e for e in entries_to_add - ) - if already_present: - skipped += 1 - continue - entries_to_add.append(self.build_base_entry(base, server)) - added += 1 - - # Дописываем новые записи одним заходом - if entries_to_add: - with open(ibases_path, "a", encoding="utf-8") as f: - for entry in entries_to_add: - f.write(entry) + a, s = self._add_entries_to_file(self.get_ibases_path(user), bases, server) + added += a + skipped += s except Exception as e: errors.append(f"{user}: {e}") + if use_zip: + zp = self.get_zip_ibases_path(user) + if zp is None: + errors.append(f"{user}: ZIP-профиль не найден") + else: + try: + a, s = self._add_entries_to_file(zp, bases, server) + zadded += a + zskipped += s + except Exception as e: + errors.append(f"{user} (ZIP): {e}") + + # Обновим панель подключённых баз, если активный пользователь среди затронутых + if self.active_user in users: + self.load_connected_bases(self.active_user) - # Итоговый отчёт summary = ( - f"Пользователей: {len(users)}, баз: {len(bases)}\n" - f"Добавлено записей: {added}\n" - f"Пропущено (уже есть): {skipped}" + f"Пользователей: {len(users)}, баз отмечено: {len(bases)}\n" + f"Локально — добавлено: {added}, пропущено (уже есть): {skipped}" ) + if use_zip: + summary += f"\nZIP-профили — добавлено: {zadded}, пропущено: {zskipped}" + self._report(summary, errors) + + def delete_connected_bases(self): + """Удаляем отмеченные подключённые базы у активного пользователя (+ ZIP по галочке).""" + user = self.active_user + if not user: + messagebox.showerror("Ошибка", "Сначала выберите пользователя слева.") + return + bases = self._get_checked(self.conn_vars) + if not bases: + messagebox.showerror("Ошибка", "Не отмечена ни одна база для удаления!") + return + + if not messagebox.askyesno( + "Подтверждение", + f"Удалить {len(bases)} баз(ы) у пользователя «{user}»?\n\n" + "\n".join(bases) + ): + return + + self.ip_address = self.ip_entry.get().strip() + use_zip = self.zip_enabled.get() + removed_local = removed_zip = 0 + errors = [] + + try: + removed_local = self._delete_entries_from_file(self.get_ibases_path(user), bases) + except Exception as e: + errors.append(f"{user}: {e}") + if use_zip: + zp = self.get_zip_ibases_path(user) + if zp is None: + errors.append(f"{user}: ZIP-профиль не найден") + else: + try: + removed_zip = self._delete_entries_from_file(zp, bases) + except Exception as e: + errors.append(f"{user} (ZIP): {e}") + + self.load_connected_bases(user) + + summary = f"Пользователь: {user}\nУдалено локально: {removed_local}" + if use_zip: + summary += f"\nУдалено в ZIP-профиле: {removed_zip}" + self._report(summary, errors) + + def _report(self, summary, errors): if errors: - summary += "\n\nОшибки:\n" + "\n".join(errors) - messagebox.showwarning("Готово с ошибками", summary) + messagebox.showwarning("Готово с ошибками", summary + "\n\nОшибки:\n" + "\n".join(errors)) else: messagebox.showinfo("Готово", summary) diff --git a/photo_2026-07-03_11-04-21-Photoroom.ico b/download.ico similarity index 53% rename from photo_2026-07-03_11-04-21-Photoroom.ico rename to download.ico index 8c7a4e1..dc935e9 100644 Binary files a/photo_2026-07-03_11-04-21-Photoroom.ico and b/download.ico differ