262 lines
11 KiB
Python
262 lines
11 KiB
Python
|
|
import os
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
import requests
|
|||
|
|
import shutil
|
|||
|
|
import tempfile
|
|||
|
|
import zipfile
|
|||
|
|
from pathlib import Path
|
|||
|
|
import sys
|
|||
|
|
import datetime
|
|||
|
|
from PySide6.QtWidgets import QMessageBox, QProgressDialog
|
|||
|
|
from PySide6.QtCore import Qt
|
|||
|
|
|
|||
|
|
# 版本检查服务器地址
|
|||
|
|
VERSION_CHECK_URL = "http://your-server.com/api/version_check"
|
|||
|
|
UPDATE_DOWNLOAD_URL = "http://your-server.com/api/download_update"
|
|||
|
|
|
|||
|
|
class VersionManager:
|
|||
|
|
"""版本管理工具类"""
|
|||
|
|
|
|||
|
|
VERSION = "1.0.0"
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def get_current_version():
|
|||
|
|
"""获取当前版本号"""
|
|||
|
|
return VersionManager.VERSION
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def check_for_updates(parent_widget=None):
|
|||
|
|
"""
|
|||
|
|
检查是否有更新可用
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
parent_widget: 父窗口,用于显示消息框
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
dict: 包含更新信息的字典,如果没有更新或检查失败则为None
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
current_version = VersionManager.get_current_version()
|
|||
|
|
logging.info(f"正在检查更新,当前版本: {current_version}")
|
|||
|
|
|
|||
|
|
# 从服务器获取最新版本信息
|
|||
|
|
response = requests.get(VERSION_CHECK_URL, params={"current_version": current_version}, timeout=5)
|
|||
|
|
response.raise_for_status() # 如果HTTP请求返回了不成功的状态码,将引发HTTPError异常
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
if not data.get("success"):
|
|||
|
|
logging.warning(f"版本检查失败: {data.get('message')}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
if data.get("update_available"):
|
|||
|
|
logging.info(f"发现新版本: {data.get('latest_version')}")
|
|||
|
|
return {
|
|||
|
|
"latest_version": data.get("latest_version"),
|
|||
|
|
"update_url": data.get("update_url"),
|
|||
|
|
"release_notes": data.get("release_notes"),
|
|||
|
|
"is_mandatory": data.get("is_mandatory", False)
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
logging.info("当前已是最新版本")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
except requests.RequestException as e:
|
|||
|
|
logging.error(f"检查更新时网络错误: {e}")
|
|||
|
|
if parent_widget:
|
|||
|
|
QMessageBox.warning(parent_widget, "更新检查失败", f"无法连接到更新服务器: {e}")
|
|||
|
|
return None
|
|||
|
|
except Exception as e:
|
|||
|
|
logging.error(f"检查更新时发生错误: {e}")
|
|||
|
|
if parent_widget:
|
|||
|
|
QMessageBox.warning(parent_widget, "更新检查失败", f"检查更新时发生错误: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def download_and_install_update(update_info, parent_widget=None):
|
|||
|
|
"""
|
|||
|
|
下载并安装更新
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
update_info: 包含更新信息的字典
|
|||
|
|
parent_widget: 父窗口,用于显示进度对话框
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
bool: 更新是否成功
|
|||
|
|
"""
|
|||
|
|
if not update_info or not update_info.get("update_url"):
|
|||
|
|
logging.error("无效的更新信息")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
update_url = update_info.get("update_url")
|
|||
|
|
version = update_info.get("latest_version")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 创建进度对话框
|
|||
|
|
progress = None
|
|||
|
|
if parent_widget:
|
|||
|
|
progress = QProgressDialog("正在下载更新...", "取消", 0, 100, parent_widget)
|
|||
|
|
progress.setWindowTitle(f"下载更新 v{version}")
|
|||
|
|
progress.setWindowModality(Qt.WindowModal)
|
|||
|
|
progress.setAutoClose(True)
|
|||
|
|
progress.setMinimumDuration(0)
|
|||
|
|
progress.setValue(0)
|
|||
|
|
progress.show()
|
|||
|
|
|
|||
|
|
# 下载更新文件
|
|||
|
|
logging.info(f"开始下载更新: {update_url}")
|
|||
|
|
|
|||
|
|
# 使用临时目录
|
|||
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|||
|
|
# 下载文件
|
|||
|
|
update_file = os.path.join(temp_dir, "update.zip")
|
|||
|
|
|
|||
|
|
# 使用流式下载以显示进度
|
|||
|
|
with requests.get(update_url, stream=True) as r:
|
|||
|
|
r.raise_for_status()
|
|||
|
|
total_size = int(r.headers.get('content-length', 0))
|
|||
|
|
downloaded = 0
|
|||
|
|
|
|||
|
|
with open(update_file, 'wb') as f:
|
|||
|
|
for chunk in r.iter_content(chunk_size=8192):
|
|||
|
|
if progress and progress.wasCanceled():
|
|||
|
|
logging.info("用户取消了下载")
|
|||
|
|
return False
|
|||
|
|
if chunk:
|
|||
|
|
f.write(chunk)
|
|||
|
|
downloaded += len(chunk)
|
|||
|
|
if total_size and progress:
|
|||
|
|
percent = int((downloaded / total_size) * 100)
|
|||
|
|
progress.setValue(percent)
|
|||
|
|
|
|||
|
|
if progress:
|
|||
|
|
progress.setValue(100)
|
|||
|
|
progress.setLabelText("正在安装更新...")
|
|||
|
|
|
|||
|
|
# 解压更新文件
|
|||
|
|
logging.info("正在解压更新文件")
|
|||
|
|
extract_dir = os.path.join(temp_dir, "extracted")
|
|||
|
|
os.makedirs(extract_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
with zipfile.ZipFile(update_file, 'r') as zip_ref:
|
|||
|
|
zip_ref.extractall(extract_dir)
|
|||
|
|
|
|||
|
|
# 获取应用程序根目录
|
|||
|
|
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
|
|||
|
|
# 备份当前版本
|
|||
|
|
current_version = VersionManager.get_current_version()
|
|||
|
|
backup_dir = os.path.join(app_dir, f"backup_v{current_version}")
|
|||
|
|
logging.info(f"备份当前版本到: {backup_dir}")
|
|||
|
|
if os.path.exists(backup_dir):
|
|||
|
|
shutil.rmtree(backup_dir)
|
|||
|
|
os.makedirs(backup_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 复制当前文件到备份目录
|
|||
|
|
for item in os.listdir(app_dir):
|
|||
|
|
if not item.startswith("backup_v"):
|
|||
|
|
s = os.path.join(app_dir, item)
|
|||
|
|
d = os.path.join(backup_dir, item)
|
|||
|
|
if os.path.isdir(s):
|
|||
|
|
shutil.copytree(s, d, dirs_exist_ok=True)
|
|||
|
|
else:
|
|||
|
|
shutil.copy2(s, d)
|
|||
|
|
|
|||
|
|
# 复制更新文件到应用目录
|
|||
|
|
logging.info("正在安装更新文件")
|
|||
|
|
for item in os.listdir(extract_dir):
|
|||
|
|
s = os.path.join(extract_dir, item)
|
|||
|
|
d = os.path.join(app_dir, item)
|
|||
|
|
if os.path.isdir(s):
|
|||
|
|
shutil.copytree(s, d, dirs_exist_ok=True)
|
|||
|
|
else:
|
|||
|
|
shutil.copy2(s, d)
|
|||
|
|
|
|||
|
|
# 更新版本文件
|
|||
|
|
version_file = os.path.join(app_dir, "version.json")
|
|||
|
|
with open(version_file, 'w') as f:
|
|||
|
|
json.dump({
|
|||
|
|
"version": version,
|
|||
|
|
"updated_at": str(datetime.datetime.now()),
|
|||
|
|
"release_notes": update_info.get("release_notes", "")
|
|||
|
|
}, f, indent=4)
|
|||
|
|
|
|||
|
|
logging.info(f"更新完成,新版本: {version}")
|
|||
|
|
|
|||
|
|
if parent_widget:
|
|||
|
|
QMessageBox.information(parent_widget, "更新完成",
|
|||
|
|
f"已成功更新到版本 {version}。\n应用程序需要重启才能应用更新。")
|
|||
|
|
|
|||
|
|
# 提示用户重启应用
|
|||
|
|
if parent_widget and QMessageBox.question(
|
|||
|
|
parent_widget, "重启应用",
|
|||
|
|
"需要重启应用程序以完成更新。是否现在重启?",
|
|||
|
|
QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
|
|||
|
|
|
|||
|
|
logging.info("用户选择重启应用")
|
|||
|
|
# 重启应用
|
|||
|
|
python = sys.executable
|
|||
|
|
os.execl(python, python, *sys.argv)
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logging.error(f"下载或安装更新时发生错误: {e}", exc_info=True)
|
|||
|
|
if parent_widget:
|
|||
|
|
QMessageBox.critical(parent_widget, "更新失败", f"下载或安装更新时发生错误: {e}")
|
|||
|
|
return False
|
|||
|
|
finally:
|
|||
|
|
if progress:
|
|||
|
|
progress.close()
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def check_and_prompt_update(parent_widget=None):
|
|||
|
|
"""检查更新并提示用户"""
|
|||
|
|
# 这里只是一个示例,实际应该连接到服务器检查更新
|
|||
|
|
logging.info(f"检查更新,当前版本: {VersionManager.VERSION}")
|
|||
|
|
return False # 返回是否有更新
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def check_and_prompt_update(parent_widget):
|
|||
|
|
"""
|
|||
|
|
检查更新并提示用户
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
parent_widget: 父窗口
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
bool: 是否有可用更新
|
|||
|
|
"""
|
|||
|
|
update_info = VersionManager.check_for_updates(parent_widget)
|
|||
|
|
if not update_info:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 获取更新信息
|
|||
|
|
latest_version = update_info.get("latest_version", "未知")
|
|||
|
|
release_notes = update_info.get("release_notes", "无更新说明")
|
|||
|
|
is_mandatory = update_info.get("is_mandatory", False)
|
|||
|
|
|
|||
|
|
message = f"发现新版本: {latest_version}\n\n"
|
|||
|
|
message += "更新内容:\n"
|
|||
|
|
message += release_notes
|
|||
|
|
|
|||
|
|
if is_mandatory:
|
|||
|
|
message += "\n\n这是一个必须安装的更新。"
|
|||
|
|
result = QMessageBox.information(parent_widget, "发现必要更新", message,
|
|||
|
|
QMessageBox.Ok | QMessageBox.Cancel)
|
|||
|
|
if result == QMessageBox.Ok:
|
|||
|
|
return VersionManager.download_and_install_update(update_info, parent_widget)
|
|||
|
|
else:
|
|||
|
|
# 如果是必要更新但用户取消,则退出应用
|
|||
|
|
if parent_widget:
|
|||
|
|
QMessageBox.critical(parent_widget, "更新取消",
|
|||
|
|
"这是一个必要更新,应用程序将退出。")
|
|||
|
|
sys.exit(0)
|
|||
|
|
else:
|
|||
|
|
message += "\n\n是否现在更新?"
|
|||
|
|
result = QMessageBox.question(parent_widget, "发现新版本", message,
|
|||
|
|
QMessageBox.Yes | QMessageBox.No)
|
|||
|
|
if result == QMessageBox.Yes:
|
|||
|
|
return VersionManager.download_and_install_update(update_info, parent_widget)
|
|||
|
|
|
|||
|
|
return False
|