Удаление баз, поиск, дублирование в ZIP-профиль + фикс скролла

- Панель «Подключённые базы»: клик по пользователю показывает его базы,
  удаление отмеченных с подтверждением
- Поиск по пользователям и базам (галочки сохраняются при фильтрации)
- Опциональное дублирование add/delete в ZIP-профиль (автопоиск папки <user>.*)
- Устойчивое чтение ibases.v8i (UTF-8 BOM/без/cp1251)
- Интерфейс переведён на русский, окно резиновое
- Фикс: сброс прокрутки списка наверх при поиске
- Новая иконка download.ico, spec теперь версионируется

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 12:11:24 +03:00
co-authored by Claude Opus 4.8
parent fdfd79346d
commit c3345a94e8
4 changed files with 441 additions and 177 deletions
-1
View File
@@ -1,4 +1,3 @@
__pycache__/ __pycache__/
build/ build/
dist/ dist/
*.spec
+39
View File
@@ -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'],
)
+401 -175
View File
@@ -1,6 +1,7 @@
import os import os
import re
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox, filedialog
import pyodbc import pyodbc
import uuid import uuid
import sys import sys
@@ -71,97 +72,154 @@ if getattr(sys, "stdout", None):
class RemoteFolderSelector(tk.Tk): class RemoteFolderSelector(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("Remote Folder and SQL Database Selector") self.title("Добавление баз 1С пользователям")
self.geometry("620x560") 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 = tk.Frame(self)
self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
self.frame_right = tk.Frame(self) self.frame_right = tk.Frame(self)
self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
# Сделаем окна масштабируемыми self._build_left_panel()
self.grid_columnconfigure(0, weight=1) self._build_right_panel()
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# Текущий режим подключения (локально либо по 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() self.load_users()
def _create_scrollable_checklist(self, parent, width=200, height=180): # ------------------------------------------------------------------ UI
"""Создаём прокручиваемую область для чекбоксов. Возвращаем (container, inner)."""
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") container = tk.Frame(parent, borderwidth=1, relief="sunken")
canvas = tk.Canvas(container, width=width, height=height, highlightthickness=0) canvas = tk.Canvas(container, width=width, height=height, highlightthickness=0)
scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview) 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) canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y") scrollbar.pack(side="right", fill="y")
# Прокрутка колесом мыши, когда курсор над областью
def _on_mousewheel(event): def _on_mousewheel(event):
canvas.yview_scroll(int(-event.delta / 120), "units") canvas.yview_scroll(int(-event.delta / 120), "units")
@@ -180,88 +237,189 @@ class RemoteFolderSelector(tk.Tk):
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>")) canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
return container, inner return container, inner
def _populate_checklist(self, inner, vars_dict, items): def _load_items(self, vars_dict, items):
"""Пересоздаём чекбоксы в области inner по списку items.""" """Создаём свежий набор BooleanVar по списку items (галочки сбрасываются)."""
for child in inner.winfo_children():
child.destroy()
vars_dict.clear() vars_dict.clear()
for item in items: for item in items:
var = tk.BooleanVar(value=False) vars_dict[item] = tk.BooleanVar(value=False)
vars_dict[item] = var
tk.Checkbutton(inner, text=item, variable=var, anchor="w").pack(fill="x", anchor="w")
def _set_all(self, vars_dict, value): def _render_checklist(self, inner, vars_dict, filter_text="", on_click=None):
"""Отмечаем (True) либо снимаем (False) все чекбоксы.""" """Рисуем чекбоксы, показывая только элементы, подходящие под фильтр.
for var in vars_dict.values(): Переменные (галочки) сохраняются между перерисовками — состояние не теряется."""
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) var.set(value)
def _get_checked(self, vars_dict): def _get_checked(self, vars_dict):
"""Возвращаем список отмеченных элементов."""
return [name for name, var in vars_dict.items() if var.get()] 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): def load_users(self):
"""Загружаем пользователей с указанного IP-адреса или локального ПК.""" """Загружаем список пользователей с локального ПК или с удалённого по IP."""
self.ip_address = self.ip_entry.get() self.ip_address = self.ip_entry.get().strip()
if not self.ip_address:
if not self.ip_address: # Если IP не указан, используем локальный ПК base = r"C:\Users"
local_path = r"C:\Users"
self.load_local_users(local_path)
else: else:
base = f"\\\\{self.ip_address}\\C$\\Users"
try: try:
# Используем указанный IP для подключения к удалённому ПК folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f))]
remote_path = f"\\\\{self.ip_address}\\C$\\Users"
self.load_remote_users(remote_path)
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Could not load users: {str(e)}") messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}")
return
def load_local_users(self, path): 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)
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)
except Exception as e:
messagebox.showerror("Error", f"Could not load local users: {str(e)}")
def load_remote_users(self, remote_path): # Сбрасываем панель подключённых баз — она относилась к прежнему источнику
"""Загружаем список пользователей с удалённого ПК.""" self.active_user = None
try: self.conn_vars.clear()
folders = [f for f in os.listdir(remote_path) if os.path.isdir(os.path.join(remote_path, f))] self._render_checklist(self.conn_inner, self.conn_vars, "")
self._populate_checklist(self.user_inner, self.user_vars, folders) self.conn_label.config(text="Подключённые базы: (выберите пользователя)")
except Exception as e:
messagebox.showerror("Error", f"Could not load remote users: {str(e)}")
def load_databases(self): def load_databases(self):
"""Подключаемся к SQL серверу и загружаем список баз данных.""" """Подключаемся к SQL-серверу и загружаем список баз."""
server = self.server_entry.get() server = self.server_entry.get().strip()
login = self.login_entry.get() login = self.login_entry.get()
password = self.password_entry.get() password = self.password_entry.get()
try: try:
connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};" connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};"
connection = pyodbc.connect(connection_string) connection = pyodbc.connect(connection_string)
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("SELECT name FROM sys.databases") cursor.execute("SELECT name FROM sys.databases")
databases = cursor.fetchall() 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() cursor.close()
connection.close() connection.close()
except Exception as e: 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): def get_ibases_path(self, user):
"""Возвращаем путь к ibases.v8i выбранного пользователя (локально либо по IP).""" """Путь к ibases.v8i пользователя (локально либо по IP)."""
if not self.ip_address: if not self.ip_address:
base_path = f"C:\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart" base_path = f"C:\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart"
else: else:
base_path = f"\\\\{self.ip_address}\\C$\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart" base_path = f"\\\\{self.ip_address}\\C$\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart"
return os.path.join(base_path, "ibases.v8i") return os.path.join(base_path, "ibases.v8i")
def get_zip_ibases_path(self, user):
"""Путь к ibases.v8i внутри ZIP-профиля пользователя.
Папку профиля ищем автопоиском: <user> либо <user>.* (например <user>.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
# Приоритет: точное <user>.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): def build_base_entry(self, base, server):
"""Формируем запись одной базы для ibases.v8i.""" """Формируем запись одной базы для ibases.v8i."""
return ( return (
@@ -280,74 +438,142 @@ class RemoteFolderSelector(tk.Tk):
"DisableLocalSpeechToText=0\n" "DisableLocalSpeechToText=0\n"
) )
def add_database_to_ibases(self): def _add_entries_to_file(self, ibases_path, bases, server):
"""Добавляем все выбранные базы всем выбранным пользователям.""" """Добавляем базы в один файл ibases.v8i (с фильтром дублей).
# Актуализируем режим (локально / по IP) на момент добавления Возвращаем (добавлено, пропущено)."""
self.ip_address = self.ip_entry.get().strip() 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) users = self._get_checked(self.user_vars)
bases = self._get_checked(self.db_vars) bases = self._get_checked(self.db_vars)
server = self.server_entry.get().strip() server = self.server_entry.get().strip()
if not users: if not users:
messagebox.showerror("Error", "Не отмечен ни один пользователь!") messagebox.showerror("Ошибка", "Не отмечен ни один пользователь!")
return return
if not bases: if not bases:
messagebox.showerror("Error", "Не отмечена ни одна база!") messagebox.showerror("Ошибка", "Не отмечена ни одна база!")
return return
if not server: if not server:
messagebox.showerror("Error", "Не указан SQL-сервер!") messagebox.showerror("Ошибка", "Не указан SQL-сервер!")
return return
added = 0 use_zip = self.zip_enabled.get()
skipped = 0 added = skipped = zadded = zskipped = 0
errors = [] errors = []
for user in users: for user in users:
try: try:
ibases_path = self.get_ibases_path(user) a, s = self._add_entries_to_file(self.get_ibases_path(user), bases, server)
base_dir = os.path.dirname(ibases_path) added += a
skipped += s
# Создаём каталог, если его нет
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)
except Exception as e: except Exception as e:
errors.append(f"{user}: {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 = ( summary = (
f"Пользователей: {len(users)}, баз: {len(bases)}\n" f"Пользователей: {len(users)}, баз отмечено: {len(bases)}\n"
f"Добавлено записей: {added}\n" f"Локально — добавлено: {added}, пропущено (уже есть): {skipped}"
f"Пропущено (уже есть): {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: if errors:
summary += "\n\nОшибки:\n" + "\n".join(errors) messagebox.showwarning("Готово с ошибками", summary + "\n\nОшибки:\n" + "\n".join(errors))
messagebox.showwarning("Готово с ошибками", summary)
else: else:
messagebox.showinfo("Готово", summary) messagebox.showinfo("Готово", summary)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB