From eb85ab35e961540a37298b7d0687203b9200bfe6 Mon Sep 17 00:00:00 2001 From: acomic <60683977+ahhhhhhhman@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:02:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0WebDAV=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=80=89=E9=A1=B9=EF=BC=8C=E6=94=AF=E6=8C=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=A4=87=E4=BB=BD=E5=92=8C=E6=81=A2=E5=A4=8D=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- ui/main_window.py | 8 ++ ui/other_settings.py | 277 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 ui/other_settings.py diff --git a/.gitignore b/.gitignore index 22eb6bb..5b766f3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ config_test.json /ui/__pycache__ .idea/ /novel -app.log \ No newline at end of file +app.log +test.py +/backup diff --git a/ui/main_window.py b/ui/main_window.py index 630e723..235c866 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -34,6 +34,8 @@ from ui.directory_tab import build_directory_tab, load_chapter_blueprint, save_c from ui.character_tab import build_character_tab, load_character_state, save_character_state from ui.summary_tab import build_summary_tab, load_global_summary, save_global_summary from ui.chapters_tab import build_chapters_tab, refresh_chapters_list, on_chapter_selected, load_chapter_content, save_current_chapter, prev_chapter, next_chapter +from ui.other_settings import build_other_settings_tab + class NovelGeneratorGUI: """ @@ -132,6 +134,10 @@ class NovelGeneratorGUI: self.scene_location_var = ctk.StringVar(value=op.get("scene_location", "")) self.time_constraint_var = ctk.StringVar(value=op.get("time_constraint", "")) self.user_guidance_default = op.get("user_guidance", "") + self.webdav_url_var = ctk.StringVar(value=op.get("webdav_url", "")) + self.webdav_username_var = ctk.StringVar(value=op.get("webdav_username", "")) + self.webdav_password_var = ctk.StringVar(value=op.get("webdav_password", "")) + else: self.topic_default = "" self.genre_var = ctk.StringVar(value="玄幻") @@ -159,6 +165,8 @@ class NovelGeneratorGUI: build_character_tab(self) build_summary_tab(self) build_chapters_tab(self) + build_other_settings_tab(self) + # ----------------- 通用辅助函数 ----------------- def show_tooltip(self, key: str): diff --git a/ui/other_settings.py b/ui/other_settings.py new file mode 100644 index 0000000..d813792 --- /dev/null +++ b/ui/other_settings.py @@ -0,0 +1,277 @@ +# ui/other_settings.py +import customtkinter as ctk +from ui.config_tab import create_label_with_help +from tkinter import messagebox +from config_manager import load_config, save_config +import requests +from requests.auth import HTTPBasicAuth +import os +from xml.etree import ElementTree as ET +import shutil +import time +def build_other_settings_tab(self): + self.other_settings_tab = self.tabview.add("Other Settings") + self.other_settings_tab.rowconfigure(0, weight=1) + self.other_settings_tab.columnconfigure(0, weight=1) + if "webdav_config" not in self.loaded_config: + self.loaded_config["webdav_config"] = { + "webdav_url": "", + "webdav_username": "", + "webdav_password": "" + } + + self.webdav_url_var.set(self.loaded_config["webdav_config"].get("webdav_url", "")) + self.webdav_username_var.set(self.loaded_config["webdav_config"].get("webdav_username", "")) + self.webdav_password_var.set(self.loaded_config["webdav_config"].get("webdav_password", "")) + + + def save_webdav_settings(): + self.loaded_config["webdav_config"]["webdav_url"] = self.webdav_url_var.get().strip() + self.loaded_config["webdav_config"]["webdav_username"] = self.webdav_username_var.get().strip() + self.loaded_config["webdav_config"]["webdav_password"] = self.webdav_password_var.get().strip() + save_config(self.loaded_config, self.config_file) + + + def test_webdav_connection(test = True): + try: + client = WebDAVClient(self.webdav_url_var.get().strip(),self.webdav_username_var.get().strip(),self.webdav_password_var.get().strip()) + client.list_directory() + if not test: + save_webdav_settings() + return True + messagebox.showinfo("成功", "WebDAV 连接成功!") + save_webdav_settings() + return True + + except Exception as e: + print(e) + + messagebox.showerror("错误", f"发生未知错误: {e}") + return False + + def backup_to_webdav(): + try: + target_dir = "AI_Novel_Generator" + client = WebDAVClient(self.webdav_url_var.get().strip(),self.webdav_username_var.get().strip(),self.webdav_password_var.get().strip()) + if not client.ensure_directory_exists(target_dir): + client.create_directory(target_dir) + client.upload_file(self.config_file, f"{target_dir}/config.json") + messagebox.showinfo("成功", "配置备份成功!") + except Exception as e: + print(e) + messagebox.showerror("错误", f"发生未知错误: {e}") + return False + + + + + + + + def restore_from_webdav(): + try: + target_dir = "AI_Novel_Generator" + client = WebDAVClient(self.webdav_url_var.get().strip(),self.webdav_username_var.get().strip(),self.webdav_password_var.get().strip()) + client.download_file(f"{target_dir}/config.json", self.config_file) + self.loaded_config = load_config(self.config_file) + messagebox.showinfo("成功", "配置恢复成功!") + + except Exception as e: + print(e) + messagebox.showerror("错误", f"发生未知错误: {e}") + return False + + + + + dav_frame = ctk.CTkFrame(self.other_settings_tab) + dav_frame.pack(padx=20, pady=20, fill="x") + + dav_title = ctk.CTkLabel(dav_frame, text="webdav设置", font=("Microsoft YaHei", 16, "bold")) + dav_title.pack(anchor="w", padx=5, pady=(0, 5)) + dav_warp_frame = ctk.CTkFrame(dav_frame, corner_radius=10, border_width=2, border_color="gray") + dav_warp_frame.pack(fill="x", padx=5) + dav_warp_frame.columnconfigure(1, weight=1) + + + + create_label_with_help(self, parent=dav_warp_frame, label_text="Webdav URL", tooltip_key="webdav_url",row=0, column=0, font=("Microsoft YaHei", 12), sticky="w") + dav_url_entry = ctk.CTkEntry(dav_warp_frame, textvariable=self.webdav_url_var, font=("Microsoft YaHei", 12)) + dav_url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w") + + create_label_with_help(self, parent=dav_warp_frame, label_text="Webdav用户名", tooltip_key="webdav_username",row=1, column=0, font=("Microsoft YaHei", 12), sticky="w") + dav_username_entry = ctk.CTkEntry(dav_warp_frame, textvariable=self.webdav_username_var, font=("Microsoft YaHei", 12)) + dav_username_entry.grid(row=1, column=1, padx=5, pady=5, sticky="w") + + create_label_with_help(self, parent=dav_warp_frame, label_text="Webdav密码", tooltip_key="webdav_password",row=2, column=0, font=("Microsoft YaHei", 12), sticky="w") + dav_password_entry = ctk.CTkEntry(dav_warp_frame, textvariable=self.webdav_password_var, font=("Microsoft YaHei", 12), show="*") + dav_password_entry.grid(row=2, column=1, padx=5, pady=5, sticky="w") + + button_frame = ctk.CTkFrame(dav_warp_frame) + button_frame.grid(row=3, column=0, columnspan=2, padx=5, pady=10, sticky="w") + + # 测试连接按钮 + test_btn = ctk.CTkButton(button_frame, text="测试连接", font=("Microsoft YaHei", 12), + command=test_webdav_connection) + test_btn.pack(side="left", padx=5) + + # 保存设置按钮 + save_btn = ctk.CTkButton(button_frame, text="备份", font=("Microsoft YaHei", 12), + command=backup_to_webdav) + save_btn.pack(side="left", padx=5) + + # 重置按钮 + reset_btn = ctk.CTkButton(button_frame, text="恢复", font=("Microsoft YaHei", 12), + command=restore_from_webdav) + reset_btn.pack(side="left", padx=5) + + + + + + + +class WebDAVClient: + def __init__(self, base_url, username, password): + """初始化WebDAV客户端""" + self.base_url = base_url.rstrip('/') + '/' + self.auth = HTTPBasicAuth(username, password) + self.headers = { + 'User-Agent': 'Python WebDAV Client', + 'Accept': '*/*' + } + # WebDAV命名空间 + self.ns = {'d': 'DAV:'} + + def _get_url(self, path): + """获取完整的资源URL""" + return self.base_url + path.lstrip('/') + + def directory_exists(self, path): + """ + 检查目录是否存在 + :param path: 目录路径 + :return: 布尔值,表示目录是否存在 + """ + url = self._get_url(path) + headers = self.headers.copy() + headers['Depth'] = '0' # 只检查当前资源 + + try: + # 发送PROPFIND请求检查资源是否存在 + response = requests.request('PROPFIND', url, headers=headers, auth=self.auth) + + # 207 Multi-Status表示成功,说明资源存在 + if response.status_code == 207: + # 解析XML响应,确认是目录 + root = ET.fromstring(response.content) + # 查找资源类型属性 + res_type = root.find('.//d:resourcetype', namespaces=self.ns) + # 如果包含collection元素,则是目录 + if res_type is not None and res_type.find('d:collection', namespaces=self.ns) is not None: + return True + return False + except requests.exceptions.RequestException as e: + print(f"检查目录存在性时出错: {e}") + return False + + def create_directory(self, path): + """ + 创建远程目录 + :param path: 要创建的目录路径 + :return: 是否创建成功 + """ + url = self._get_url(path) + + try: + response = requests.request('MKCOL', url, auth=self.auth, headers=self.headers) + response.raise_for_status() + + print(f"目录创建成功: {path}") + return True + except requests.exceptions.RequestException as e: + print(f"目录创建失败: {e}") + return False + + def ensure_directory_exists(self, path): + """ + 确保目录存在,如果不存在则创建 + :param path: 目录路径 + :return: 布尔值,表示最终目录是否存在 + """ + # 移除末尾的斜杠(如果有) + path = path.rstrip('/') + + # 如果目录已经存在,直接返回True + if self.directory_exists(path): + print(f"目录已存在: {path}") + return True + + # 递归创建父目录 + parent_dir = os.path.dirname(path) + if parent_dir and not self.directory_exists(parent_dir): + # 如果父目录不存在,则先创建父目录 + if not self.ensure_directory_exists(parent_dir): + print(f"创建父目录失败: {parent_dir}") + return False + + # 创建当前目录 + return self.create_directory(path) + def upload_file(self, local_path, remote_path): + """ + 上传文件到WebDAV服务器 + :param local_path: 本地文件路径 + :param remote_path: 远程文件路径 + :return: 是否上传成功 + """ + if not os.path.isfile(local_path): + print(f"本地文件不存在: {local_path}") + return False + + url = self._get_url(remote_path) + + try: + with open(local_path, 'rb') as f: + response = requests.put(url, data=f, auth=self.auth, headers=self.headers) + response.raise_for_status() + + print(f"文件上传成功: {local_path} -> {remote_path}") + return True + except requests.exceptions.RequestException as e: + print(f"文件上传失败: {e}") + return False + def download_file(self, remote_path, local_path): + """ + 从WebDAV服务器下载文件 + :param remote_path: 远程文件路径 + :param local_path: 本地保存路径 + :return: 是否下载成功 + """ + url = self._get_url(remote_path) + local_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), local_path) + self.backup(local_path) + try: + response = requests.get(url, auth=self.auth, headers=self.headers, stream=True) + response.raise_for_status() + + # 创建本地目录(如果需要) + os.makedirs(os.path.dirname(local_path), exist_ok=True) + + with open(local_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + print(f"文件下载成功: {remote_path} -> {local_path}") + return True + except requests.exceptions.RequestException as e: + print(f"文件下载失败: {e}") + return False + def backup(self, local_path): + name_parts = os.path.basename(local_path).rsplit('.', 1) # 只分割最后一个点 + base_name = name_parts[0] + extension = name_parts[1] + timestamp = time.strftime("%Y%m%d%H%M%S") + if not os.path.exists(os.path.join(os.path.dirname(local_path), "backup")): + os.makedirs(os.path.join(os.path.dirname(local_path), "backup")) + backup_file_name = f"{base_name}_{timestamp}_bak.{extension}" + shutil.copy2(os.path.basename(local_path), os.path.join(os.path.dirname(local_path), "backup", backup_file_name))