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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:11:24 +03:00

584 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import re
import tkinter as tk
from tkinter import messagebox, filedialog
import pyodbc
import uuid
import sys
_BANNER = ("""
....:::::::::
::::::::::-----::::::
::::------------=--------::..
:::--=--============---==+===-:::.
::---========++=++++======+++++==--:.:
:--==++++++++++++++++++========---==-::.
--==++++++++++++++++++++=====---:::-==--::
:-==+**++++++++++++++++++====------::-==-::
-==+**++++++++++++++++++++=====--------=-::
-=++*++++++++++++++++++++++=========-----::
-=+++++++++++++++++++++++++===========----:
:=++++++++++++++++++++++++++==========---::
-=++++++++++++++++++===+++++===-----====--
-=++++++++++++++++++++==+++++=====-----==-
-++++++++++++++++++++++++++++++++====-----
==+++++++*#####*******++++++++++++++++=--=
+===++++++****##%%%%##*****++++**###%##*+==-
+*++=++++****#%##%@%%%%###***++*#%%%%%###*===-
+*++=++++**####*#%@%#%%%##**++*#%%%@@%+*%%*+=-
*++=+++++**+****####%%##*++==+#%%#%%#+++*+==-
+*+++++++++++****###****++=====*####*++=====-
+*=+++++++++++++++++++++++=======+**++=-----
+*=+++++++++++++++++***++++==-=====+++==----
++=+++++++++++++++*****+++==---===========--
+=+++++++++++++*****+++++===---++======--=-
==++++++++++**********+++++==--+++++=====-
-==+++++++++*********##*******==++++++===-
-==++++++++*****#**##*####**+====++++++==
#%%%%%===+++++++++***##*****####**++++++==+++=-
*+*#%%%#*+====++++++++**#%%%##****##********+====-
++++++*##%@%#+++**====+++++++*##############***###*+====-
++******###%%@@@@@%####*+=+==+++++**************+++++=+*++===-
#***+**#*###%@@@@@@@@@@@@@%###*+=+==++++*****++*********++==++++=-=+=+=-
#*#%%%###%%@@@@@@@@@@@@@@@@%###*++++++++**+************+++========*****++==-
%%%######%%####%@@@@@@@@@@@@@@%%**+++++++++++++++***+++=========*###******+++++==
%%@@@@%%%%%%%%%#%%@@@@@@@@@@@@@@%#**+*+**++++++++++++++++=====+%###########***++++*
%@@@@@@@@@%%@@%%%%%%%@@@@@@@@@@@@@@###*****++++++*+++++++===+**#%####%###%####***+***
%%%@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@%####**++++***++++*#####*%##%####%#%%%%##*******
%%%%@%@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@%%%%%%##%%%@%%####*%%#%####%#%%%%%#########
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%##%#*#%#%####%##%%%%%%###%#%##
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%#%%###%######%##%#%%%%%%%%%%%##
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%####%%#####%##%%%%%%%%%%%%%###
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%####%%#%%##%#%%%%%%%%%%@%@%###
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%#####%%%#%%%%#%%%%%%%@@@@@@@%%##
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%#%%%%##%%#%%%%%%%%%%%%@@@@@@@%%#**
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%@@@%####%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@%%%#**
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%@@@@%%%%%%%#%%%%%%%##%%%%%%%%%%%%%%%%%@@@@@@%%%%##**
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%@@@@%%@@@@@@@%%%%%%##%%%%%%%###%%%%%%%%%%%%%%%%%@@%%%%%%%%##*+
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%@@@@@@@@%%%%@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%###%%*#
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@%@@@%@@@@@@@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%
""")
# В оконном режиме (сборка без консоли) sys.stdout может быть None — не роняем старт
if getattr(sys, "stdout", None):
try:
print(_BANNER)
except Exception:
pass
class RemoteFolderSelector(tk.Tk):
def __init__(self):
super().__init__()
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._build_left_panel()
self._build_right_panel()
# Большая кнопка добавления снизу, на всю ширину окна
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")
# По умолчанию грузим пользователей локального ПК
self.load_users()
# ------------------------------------------------------------------ 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)
inner = tk.Frame(canvas)
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=inner, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
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")
canvas.bind("<Enter>", lambda e: canvas.bind_all("<MouseWheel>", _on_mousewheel))
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
return container, inner
def _load_items(self, vars_dict, items):
"""Создаём свежий набор BooleanVar по списку items (галочки сбрасываются)."""
vars_dict.clear()
for item in items:
vars_dict[item] = tk.BooleanVar(value=False)
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().strip()
if not self.ip_address:
base = r"C:\Users"
else:
base = f"\\\\{self.ip_address}\\C$\\Users"
try:
folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f))]
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}")
return
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().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()
cursor.close()
connection.close()
except Exception as 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)."""
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 (
f"\n[{base}]\n"
f'Connect=Srvr="{server}";Ref="{base}";\n'
f"ID={uuid.uuid4()}\n"
"OrderInList=255\n"
"Folder=/\n"
"OrderInTree=16640\n"
"External=0\n"
"ClientConnectionSpeed=Normal\n"
"App=Auto\n"
"WA=1\n"
"Version=8.3\n"
"DefaultApp=ThickClient\n"
"DisableLocalSpeechToText=0\n"
)
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("Ошибка", "Не отмечен ни один пользователь!")
return
if not bases:
messagebox.showerror("Ошибка", "Не отмечена ни одна база!")
return
if not server:
messagebox.showerror("Ошибка", "Не указан SQL-сервер!")
return
use_zip = self.zip_enabled.get()
added = skipped = zadded = zskipped = 0
errors = []
for user in users:
try:
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}, пропущено (уже есть): {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:
messagebox.showwarning("Готово с ошибками", summary + "\n\nОшибки:\n" + "\n".join(errors))
else:
messagebox.showinfo("Готово", summary)
if __name__ == "__main__":
app = RemoteFolderSelector()
app.mainloop()