Удаление баз, поиск, дублирование в 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__/
build/
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 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("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
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:
base = f"\\\\{self.ip_address}\\C$\\Users"
try:
# Используем указанный IP для подключения к удалённому ПК
remote_path = f"\\\\{self.ip_address}\\C$\\Users"
self.load_remote_users(remote_path)
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 users: {str(e)}")
messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}")
return
def load_local_users(self, path):
"""Загружаем список пользователей с локального ПК."""
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)}")
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)
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.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-профиля пользователя.
Папку профиля ищем автопоиском: <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):
"""Формируем запись одной базы для 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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB