328 lines
14 KiB
Python
328 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import queue
|
|
import threading
|
|
from pathlib import Path
|
|
from tkinter import BooleanVar, StringVar, Tk, filedialog, messagebox, ttk
|
|
from tkinter.scrolledtext import ScrolledText
|
|
|
|
from sync_core import (
|
|
FileLogger,
|
|
copy_unc_missing_assets,
|
|
import_disk_assets_to_server,
|
|
normalize_path,
|
|
parse_path_list,
|
|
scan_assets,
|
|
stamp_text,
|
|
write_inventory,
|
|
)
|
|
|
|
|
|
APP_DIR = Path(__file__).resolve().parent
|
|
CONFIG_PATH = APP_DIR / "config.json"
|
|
|
|
DEFAULT_CONFIG = {
|
|
"server_asset_roots": r"D:\LuTan1_Image_Pool_Zip" + "\n" + r"D:\LT1_data_lsarorbit",
|
|
"inventory_output": "",
|
|
"server_inventory_json": "",
|
|
"unc_roots": "",
|
|
"unc_copy_targets": "",
|
|
"disk_roots": "",
|
|
"import_server_roots": r"D:\LuTan1_Image_Pool_Zip" + "\n" + r"D:\LT1_data_lsarorbit",
|
|
"report_dir": "",
|
|
"move_on_import": False,
|
|
}
|
|
|
|
|
|
class LT1AssetTool:
|
|
def __init__(self, root: Tk) -> None:
|
|
self.root = root
|
|
self.root.title("LT1 资产搬运工具")
|
|
self.root.geometry("1120x820")
|
|
self.log_queue: queue.Queue[str] = queue.Queue()
|
|
self.worker: threading.Thread | None = None
|
|
self.text_widgets: dict[str, ScrolledText] = {}
|
|
|
|
config = self.load_config()
|
|
self.vars = {
|
|
"server_asset_roots": StringVar(value=config["server_asset_roots"]),
|
|
"inventory_output": StringVar(value=config["inventory_output"]),
|
|
"server_inventory_json": StringVar(value=config["server_inventory_json"]),
|
|
"unc_roots": StringVar(value=config["unc_roots"]),
|
|
"unc_copy_targets": StringVar(value=config["unc_copy_targets"]),
|
|
"disk_roots": StringVar(value=config["disk_roots"]),
|
|
"import_server_roots": StringVar(value=config["import_server_roots"]),
|
|
"report_dir": StringVar(value=config["report_dir"]),
|
|
}
|
|
self.move_on_import = BooleanVar(value=bool(config["move_on_import"]))
|
|
self.summary_text = StringVar(value="准备就绪")
|
|
self.build_ui()
|
|
self.root.after(100, self.drain_logs)
|
|
|
|
def load_config(self) -> dict:
|
|
if not CONFIG_PATH.exists():
|
|
return dict(DEFAULT_CONFIG)
|
|
try:
|
|
payload = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
return {**DEFAULT_CONFIG, **payload}
|
|
except Exception:
|
|
return dict(DEFAULT_CONFIG)
|
|
|
|
def sync_text_vars(self) -> None:
|
|
for key, widget in self.text_widgets.items():
|
|
self.vars[key].set(widget.get("1.0", "end").strip())
|
|
|
|
def save_config(self) -> None:
|
|
self.sync_text_vars()
|
|
payload = {key: var.get() for key, var in self.vars.items()}
|
|
payload["move_on_import"] = self.move_on_import.get()
|
|
CONFIG_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
def build_ui(self) -> None:
|
|
outer = ttk.Frame(self.root, padding=12)
|
|
outer.pack(fill="both", expand=True)
|
|
|
|
common = ttk.LabelFrame(outer, text="公共输出", padding=10)
|
|
common.pack(fill="x")
|
|
self.add_path_row(common, 0, "报告/日志目录", "report_dir")
|
|
|
|
scan_box = ttk.LabelFrame(outer, text="1. 扫描服务器资产路径,生成随身 JSON", padding=10)
|
|
scan_box.pack(fill="x", pady=(10, 0))
|
|
self.add_multi_path_row(scan_box, 0, "服务器资产路径(多个用分号或换行)", "server_asset_roots")
|
|
self.add_file_save_row(scan_box, 1, "服务器资产 JSON 保存为", "inventory_output")
|
|
ttk.Button(scan_box, text="执行 1:扫描服务器并生成 JSON", command=lambda: self.start_worker(self.run_scan_server)).grid(
|
|
row=2,
|
|
column=1,
|
|
sticky="w",
|
|
pady=(8, 0),
|
|
)
|
|
|
|
unc_box = ttk.LabelFrame(outer, text="2. 扫描 UNC 多路径,按服务器 JSON 跳过已有,剩余复制到指定路径", padding=10)
|
|
unc_box.pack(fill="x", pady=(10, 0))
|
|
self.add_file_row(unc_box, 0, "读取服务器资产 JSON", "server_inventory_json")
|
|
self.add_multi_path_row(unc_box, 1, "UNC 源路径(多个用分号或换行)", "unc_roots")
|
|
self.add_multi_path_row(unc_box, 2, "复制目标路径(多个用分号或换行)", "unc_copy_targets")
|
|
ttk.Button(unc_box, text="预览 2:只生成复制报告", command=lambda: self.start_worker(lambda logger: self.run_unc_copy(logger, execute=False))).grid(
|
|
row=3,
|
|
column=1,
|
|
sticky="w",
|
|
pady=(8, 0),
|
|
)
|
|
ttk.Button(unc_box, text="执行 2:复制缺失资产", command=lambda: self.start_worker(lambda logger: self.run_unc_copy(logger, execute=True))).grid(
|
|
row=3,
|
|
column=1,
|
|
sticky="w",
|
|
padx=(180, 0),
|
|
pady=(8, 0),
|
|
)
|
|
|
|
import_box = ttk.LabelFrame(outer, text="3. 将指定磁盘路径资产复制或剪切到服务器资产路径", padding=10)
|
|
import_box.pack(fill="x", pady=(10, 0))
|
|
self.add_multi_path_row(import_box, 0, "磁盘资产路径(多个用分号或换行)", "disk_roots")
|
|
self.add_multi_path_row(import_box, 1, "服务器资产路径(多个用分号或换行)", "import_server_roots")
|
|
ttk.Checkbutton(import_box, text="剪切到服务器(不勾选则复制)", variable=self.move_on_import).grid(row=2, column=1, sticky="w")
|
|
ttk.Button(import_box, text="预览 3:只生成导入报告", command=lambda: self.start_worker(lambda logger: self.run_import(logger, execute=False))).grid(
|
|
row=3,
|
|
column=1,
|
|
sticky="w",
|
|
pady=(8, 0),
|
|
)
|
|
ttk.Button(import_box, text="执行 3:导入到服务器", command=lambda: self.start_worker(lambda logger: self.run_import(logger, execute=True))).grid(
|
|
row=3,
|
|
column=1,
|
|
sticky="w",
|
|
padx=(180, 0),
|
|
pady=(8, 0),
|
|
)
|
|
ttk.Label(import_box, text="执行 3 后,请回到服务器重新执行 1,生成新的随身 JSON。", foreground="#92400e").grid(
|
|
row=4,
|
|
column=1,
|
|
sticky="w",
|
|
pady=(8, 0),
|
|
)
|
|
|
|
control = ttk.Frame(outer)
|
|
control.pack(fill="x", pady=10)
|
|
ttk.Button(control, text="保存配置", command=self.handle_save_config).pack(side="left")
|
|
ttk.Label(control, textvariable=self.summary_text, font=("Microsoft YaHei UI", 10, "bold")).pack(side="left", padx=16)
|
|
|
|
log_box = ttk.LabelFrame(outer, text="日志", padding=10)
|
|
log_box.pack(fill="both", expand=True)
|
|
self.log_view = ScrolledText(log_box, height=14, wrap="word")
|
|
self.log_view.pack(fill="both", expand=True)
|
|
|
|
def add_path_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
|
|
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
|
|
ttk.Entry(frame, textvariable=self.vars[key], width=105).grid(row=row, column=1, sticky="ew", pady=4)
|
|
ttk.Button(frame, text="选择", command=lambda: self.choose_dir(key)).grid(row=row, column=2, padx=(8, 0), pady=4)
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
def add_file_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
|
|
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
|
|
ttk.Entry(frame, textvariable=self.vars[key], width=105).grid(row=row, column=1, sticky="ew", pady=4)
|
|
ttk.Button(frame, text="选择", command=lambda: self.choose_file(key)).grid(row=row, column=2, padx=(8, 0), pady=4)
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
def add_file_save_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
|
|
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
|
|
ttk.Entry(frame, textvariable=self.vars[key], width=105).grid(row=row, column=1, sticky="ew", pady=4)
|
|
ttk.Button(frame, text="选择", command=lambda: self.choose_save_file(key)).grid(row=row, column=2, padx=(8, 0), pady=4)
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
def add_multi_path_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
|
|
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="nw", padx=(0, 8), pady=4)
|
|
text = ScrolledText(frame, width=105, height=3, wrap="none")
|
|
text.insert("1.0", self.vars[key].get())
|
|
text.grid(row=row, column=1, sticky="ew", pady=4)
|
|
self.text_widgets[key] = text
|
|
ttk.Button(frame, text="追加目录", command=lambda: self.append_dir(key)).grid(row=row, column=2, padx=(8, 0), pady=4, sticky="n")
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
def choose_dir(self, key: str) -> None:
|
|
selected = filedialog.askdirectory()
|
|
if selected:
|
|
self.vars[key].set(selected)
|
|
|
|
def append_dir(self, key: str) -> None:
|
|
selected = filedialog.askdirectory()
|
|
if not selected:
|
|
return
|
|
widget = self.text_widgets.get(key)
|
|
if not widget:
|
|
self.vars[key].set(selected)
|
|
return
|
|
current = widget.get("1.0", "end").strip()
|
|
widget.delete("1.0", "end")
|
|
widget.insert("1.0", f"{current}\n{selected}" if current else selected)
|
|
|
|
def choose_file(self, key: str) -> None:
|
|
selected = filedialog.askopenfilename(filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")])
|
|
if selected:
|
|
self.vars[key].set(selected)
|
|
|
|
def choose_save_file(self, key: str) -> None:
|
|
selected = filedialog.asksaveasfilename(
|
|
defaultextension=".json",
|
|
filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")],
|
|
initialfile=f"server_assets_{stamp_text()}.json",
|
|
)
|
|
if selected:
|
|
self.vars[key].set(selected)
|
|
|
|
def report_dir(self) -> Path:
|
|
text = self.vars["report_dir"].get().strip()
|
|
if not text:
|
|
raise ValueError("请指定报告/日志目录")
|
|
return normalize_path(text)
|
|
|
|
def logger(self) -> FileLogger:
|
|
return FileLogger(self.report_dir() / "logs" / f"run_{stamp_text()}.log", self.log)
|
|
|
|
def handle_save_config(self) -> None:
|
|
self.save_config()
|
|
messagebox.showinfo("已保存", f"配置已保存到:{CONFIG_PATH}")
|
|
|
|
def log(self, message: str) -> None:
|
|
self.log_queue.put(message)
|
|
|
|
def drain_logs(self) -> None:
|
|
while True:
|
|
try:
|
|
message = self.log_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
self.log_view.insert("end", message + "\n")
|
|
self.log_view.see("end")
|
|
self.root.after(100, self.drain_logs)
|
|
|
|
def start_worker(self, action) -> None:
|
|
if self.worker and self.worker.is_alive():
|
|
messagebox.showwarning("正在运行", "已有任务正在运行。")
|
|
return
|
|
self.save_config()
|
|
self.log_view.delete("1.0", "end")
|
|
self.worker = threading.Thread(target=self.run_action, args=(action,), daemon=True)
|
|
self.worker.start()
|
|
|
|
def run_action(self, action) -> None:
|
|
try:
|
|
action(self.logger())
|
|
except Exception as exc:
|
|
self.log(f"任务失败:{exc}")
|
|
self.root.after(0, lambda: messagebox.showerror("任务失败", str(exc)))
|
|
|
|
def run_scan_server(self, logger: FileLogger) -> None:
|
|
roots = parse_path_list(self.vars["server_asset_roots"].get())
|
|
if not roots:
|
|
raise ValueError("请填写服务器资产路径")
|
|
output = self.vars["inventory_output"].get().strip()
|
|
if not output:
|
|
raise ValueError("请指定服务器资产 JSON 保存路径")
|
|
logger("开始扫描服务器资产路径")
|
|
assets = scan_assets(roots, log=logger)
|
|
write_inventory(normalize_path(output), assets, [str(root) for root in roots])
|
|
self.root.after(0, lambda: self.summary_text.set(f"服务器资产 {len(assets)} 个,JSON 已生成"))
|
|
logger(f"完成:{output}")
|
|
|
|
def run_unc_copy(self, logger: FileLogger, *, execute: bool) -> None:
|
|
inventory = self.vars["server_inventory_json"].get().strip()
|
|
unc_roots = parse_path_list(self.vars["unc_roots"].get())
|
|
targets = parse_path_list(self.vars["unc_copy_targets"].get())
|
|
if not inventory:
|
|
raise ValueError("请指定服务器资产 JSON")
|
|
if not unc_roots:
|
|
raise ValueError("请填写 UNC 源路径")
|
|
if not targets:
|
|
raise ValueError("请填写复制目标路径")
|
|
logger("开始扫描 UNC 并按服务器 JSON 跳过已有资产")
|
|
report = copy_unc_missing_assets(
|
|
unc_roots,
|
|
normalize_path(inventory),
|
|
targets,
|
|
self.report_dir() / "reports",
|
|
execute=execute,
|
|
log=logger,
|
|
)
|
|
copied = sum(1 for item in report if item.action == "copied")
|
|
planned = sum(1 for item in report if item.action == "planned")
|
|
skipped = sum(1 for item in report if item.action == "skip")
|
|
failed = sum(1 for item in report if item.action == "failed")
|
|
text = f"UNC 处理完成:copied={copied}, planned={planned}, skipped={skipped}, failed={failed}"
|
|
self.root.after(0, lambda: self.summary_text.set(text))
|
|
logger(text)
|
|
|
|
def run_import(self, logger: FileLogger, *, execute: bool) -> None:
|
|
disk_roots = parse_path_list(self.vars["disk_roots"].get())
|
|
server_roots = parse_path_list(self.vars["import_server_roots"].get())
|
|
if not disk_roots:
|
|
raise ValueError("请填写磁盘资产路径")
|
|
if not server_roots:
|
|
raise ValueError("请填写服务器资产路径")
|
|
logger("开始从磁盘导入资产到服务器")
|
|
report = import_disk_assets_to_server(
|
|
disk_roots,
|
|
server_roots,
|
|
self.report_dir() / "reports",
|
|
execute=execute,
|
|
move=self.move_on_import.get(),
|
|
log=logger,
|
|
)
|
|
imported = sum(1 for item in report if item.action in {"copied", "moved"})
|
|
planned = sum(1 for item in report if item.action == "planned")
|
|
skipped = sum(1 for item in report if item.action == "skip")
|
|
failed = sum(1 for item in report if item.action == "failed")
|
|
text = f"磁盘导入完成:imported={imported}, planned={planned}, skipped={skipped}, failed={failed}。请重新执行 1。"
|
|
self.root.after(0, lambda: self.summary_text.set(text))
|
|
logger(text)
|
|
|
|
|
|
def main() -> None:
|
|
root = Tk()
|
|
LT1AssetTool(root)
|
|
root.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|