自动更新版本流程
1. 检测新版本,弹窗提醒更新 2.下载更新文件压缩包 3.将旧的配置文件移到新配置文件夹中 4.关闭当前进程,移除旧文件夹里面的文件,将新文件移到当前位置 5.启动新程序
dos 命令执行删除时需要慎重,尽量使用移动到一个old_file里面
# 存储版本的表
CREATE TABLE `plugin_version` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`version` varchar(100) DEFAULT NULL COMMENT '版本',
`file_name` varchar(255) DEFAULT NULL COMMENT '软件名',
`remark` text COMMENT '更新描述',
`is_force_update` tinyint(4) DEFAULT '0' COMMENT '0.可选择更新 1. 强制更新',
`soft_type` tinyint(2) DEFAULT NULL COMMENT '软件类型1.软件测试 2.打印机插件3.浏览器',
`zip_url` varchar(255) DEFAULT NULL COMMENT '当前版本链接URL',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `create_time` (`create_time`),
KEY `soft_type` (`soft_type`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='自动更新表';
# -*- coding: utf-8 -*-
import json
import subprocess
import sys
import textwrap
import threading
# import webbrowser
import zipfile
from pathlib import os
# import os
import requests
import shutil
import semver
import tkinter
import tkinter.messagebox
from tqdm.tk import tqdm
from tkinter import Tk
import py7zr
import concurrent.futures
from threading import Thread
"""
pyinstaller -D unit_test.py -n '软件测试'
"""
class Unit_Test_Version():
def __init__(self, root=None):
# 当前版本号
self.current_version = '1.0.0'
# 请求软件资源信息
self.version_url = 'http://127.0.0.1:19315/search_version'
# 软件名
self.app_name = '软件测试'
# 软件类型(同一个表存多个软件的版本)
self.soft_type = 1
self.root = root
def upzip_file(self, zip_path=None, unzip_path=None):
"""
:zip_path 压缩文件路径
:unzip_path 解压文件路径
:return 解压 zip 文件,返回所有解压文件夹下的路径
"""
zip_file = zipfile.ZipFile(zip_path)
if not os.path.isdir(unzip_path):
os.mkdir(unzip_path)
for names in zip_file.namelist():
zip_file.extract(names, unzip_path)
zip_file.close()
return [os.path.join(unzip_path, i).replace('\\', '/') for i in zip_file.namelist()]
def upzip_file_new0(self, zip_path=None, unzip_path=None):
paths = []
if not os.path.exists(unzip_path):
os.mkdir(unzip_path)
with zipfile.ZipFile(file=zip_path, mode='r') as zf:
for old_name in zf.namelist():
file_size = zf.getinfo(old_name).file_size
# 由于源码遇到中文是cp437方式,所以解码成gbk,windows即可正常
new_name = old_name.encode('cp437').decode('gbk')
# 拼接文件的保存路径
new_path = os.path.join(unzip_path, new_name)
paths.append(new_path)
# 判断文件是文件夹还是文件
if file_size > 0:
# 是文件,通过open创建文件,写入数据
with open(file=new_path, mode='wb') as f:
# zf.read 是读取压缩包里的文件内容
f.write(zf.read(old_name))
else:
# 是文件夹,就创建
os.mkdir(new_path)
return paths
def execute_unzip(self, zf, old_name, unzip_path):
# 由于源码遇到中文是cp437方式,所以解码成gbk,windows即可正常
new_name = old_name.encode('cp437').decode('gbk')
# 拼接文件的保存路径
new_path = os.path.join(unzip_path, new_name)
# 是文件,通过open创建文件,写入数据
with open(file=new_path, mode='wb') as f:
# zf.read 是读取压缩包里的文件内容
f.write(zf.read(old_name))
def list_of_groups(self, list_info, per_list_len):
"""
:param list_info: 大列表
:param per_list_len: 每个小列表数量
:return: 将一个列表切分成多个小列表
"""
list_of_group = zip(*(iter(list_info),) * per_list_len)
end_list = [list(i) for i in list_of_group]
count = len(list_info) % per_list_len
end_list.append(list_info[-count:]) if count != 0 else end_list
return end_list
def upzip_file_new(self, zip_path, unzip_path):
print('开始解压文件')
# 多线程解压
if not os.path.exists(unzip_path):
os.mkdir(unzip_path)
with zipfile.ZipFile(file=zip_path, mode='r') as zf:
files_names = []
for old_name in zf.namelist():
# 区分文件夹还是文件
is_dir = zf.getinfo(old_name).is_dir()
# 由于源码遇到中文是cp437方式,所以解码成gbk
new_name = old_name.encode('cp437').decode('gbk')
# 拼接文件的保存路径
new_path = os.path.join(unzip_path, new_name)
# print(f'file_size: {is_dir} new_name: {new_name} {is_dir}')
if not is_dir:
files_names.append(old_name)
else:
# 是文件夹,就创建
if not os.path.exists(new_path):
os.mkdir(new_path)
lists = self.list_of_groups(files_names, 20)
for data in lists:
tasks = []
for old_name in data:
task = threading.Thread(target=self.execute_unzip, args=(zf, old_name, unzip_path))
task.start()
tasks.append(task)
for t in tasks:
t.join()
def extract_7z_file(self, file_path, output_dir):
print('解压.7z文件')
if not os.path.exists(output_dir):
os.mkdir(output_dir)
t1 = time.time()
with py7zr.SevenZipFile(file_path, mode='r') as z:
z.extractall(path=output_dir)
t2 = time.time()
print(f'解压耗时: {t2-t1}')
def get_version_info(self):
# 获取版本信息
try:
data = json.dumps({'soft_type': self.soft_type})
res = requests.post(self.version_url, data=data, timeout=15)
response = json.loads(res.text)
except:
response = {}
return response
def del_current_pid(self):
pid = os.getpid() # 关闭后台进程
try:
os.system('taskkill -f -pid %s' % pid)
except:
pass
def check_version(self, version_info):
self.root = Tk()
self.root.withdraw() # 隐藏主窗口
t = threading.Thread(target=lambda: self.check_for_update(version_info), name='update_thread')
t.daemon = True # 守护为True,设置True线程会随着进程一同关闭
t.start()
self.root.mainloop()
def check_for_update(self, version_info):
version = version_info['version']
ver = semver.compare(version, self.current_version)
if ver:
# 更新的内容
self.is_down_update = True
publish_notes = version_info['remark']
message = f'当前版本[{self.current_version}], 有新版本[{version}]\n' \
f'更新内容:\n{publish_notes}\n请选择立即去下载更新[确定],暂不更新[取消]?'
# 修改窗口图标
# self.root.iconbitmap('xxxx.ico')
result = tkinter.messagebox.askokcancel(title='更新提示', message=message)
if result:
self.down_zip(version_info)
self.root.destroy()
else:
self.is_down_update = False
self.root.destroy()
else:
self.is_down_update = False
self.root.destroy()
def down_zip(self, version_info):
zip_url, file_name = version_info['zip_url'], version_info['file_name']
res = requests.get(zip_url)
# 当前文件夹路径
# current_path = os.path.dirname(os.path.realpath(sys.argv[0]))
# 当前文件夹上一级路径
current_path = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
zip_path = os.path.join(current_path, f'{zip_url.split("/")[-1]}')
unzip_path = os.path.join(current_path, file_name+'_new')
print('zip_path:', zip_path)
print('unzip_path:', unzip_path)
# with open(zip_path, 'wb') as f:
# f.write(res.content)
chunk_size = 1024 * 100
total_size = int(res.headers.get('content-length', 0))
progress_bar = tqdm(iterable=res.iter_content(chunk_size=chunk_size), tk_parent=None,
leave=False, total=total_size, unit='B', unit_scale=True)
# 设置下载进度条位置
screenWidth = progress_bar._tk_window.winfo_screenwidth()
screenHeight = progress_bar._tk_window.winfo_screenheight()
progress_bar._tk_window.geometry(f"300x160+{screenWidth - 300}+{screenHeight - 400}")
try:
with open(zip_path, 'wb') as f:
for chunk in res.iter_content(chunk_size):
if chunk:
f.write(chunk)
progress_bar.update(len(chunk))
except Exception as ee:
progress_bar.close()
return '下载异常,检查链接'
progress_bar.close()
# 解压压缩包
#filepath = self.upzip_file_new0(zip_path, unzip_path)[0]
if zip_path.endswith('.zip'):
self.upzip_file_new(zip_path, unzip_path)
else:
self.extract_7z_file(zip_path, unzip_path)
filepath = os.path.join(unzip_path, zip_url.replace('.zip', '').replace('.7z', '').split('/')[-1])
print('解压压缩包成功filepath:', filepath)
print('filepath:', filepath)
# 删除压缩包
if os.path.exists(zip_path):
os.remove(zip_path)
# 老版本配置文件
#old_config = os.path.join(current_path, 'config', 'config.ini')
# 新版本的配置文件路径
#new_config = os.path.join(filepath, '客户端配置文件.ini')
#if os.path.exists(new_config) and os.path.exists(old_config):
#os.remove(new_config)
#if os.path.exists(old_config):
#if not os.path.exists(os.path.join(filepath, 'config')):
#os.makedirs(os.path.join(filepath, 'config'))
# shutil.copy(old_config, new_config)
self.auto_update_setup(filepath)
def make_updater_bat(self, filepath):
filepath = os.path.abspath(filepath)
# 当前进程ID
pid = os.getpid()
current_path = os.path.dirname(os.path.realpath(sys.argv[0]))
print('current_path:', current_path)
# 删除临时文件夹路径
new_file_dir = os.path.abspath(os.path.join(filepath, ".."))
# old_file = filepath.replace(new_file_dir.replace('\\', '/'), current_path.replace('\\', '/')) # 旧的文件夹
# old_file = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
old_file = current_path
old_file_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # 上一级文件夹
new_file = filepath # 新版本文件夹
# 运行exe路径
# new_file_exe = os.path.join(filepath, f'{self.app_name}.exe').replace('\\', '/')
# new_file_exe = new_file_exe.replace(new_file_dir.replace('\\', '/'), old_file_dir.replace('\\', '/'))
new_file_exe = os.path.join(current_path, f'{self.app_name}.exe').replace('\\', '/')
print('filepath:', filepath)
print('new_file_dir:', new_file_dir.replace('\\', '/'))
print('current_path:', current_path.replace('\\', '/'))
# 自动更新脚本
"""
关闭当前进程,删除所有文件除了updater.bat,将新版本下的所有文件移到当前文件夹下,访问新exe文件
"""
bat_name = 'updater.bat'
bat_path = os.path.join(old_file, bat_name)
print('bat_path:', bat_path)
# 比如不保留配置文件config和coreDirectory,则如下
with open(bat_path, 'w', encoding='utf-8') as updater:
updater.write(textwrap.dedent(f'''\
@echo off & setlocal enabledelayedexpansion
CHCP 65001
echo 正在更新[{self.app_name}]最新版本,请勿关闭窗口...
ping -n 2 127.0.0.1
taskkill -f -pid {pid}
echo 正在复制[{self.app_name}],请勿关闭窗口...
echo dos命令路径中包含中文时IF EXIST不起作用,添加 CHCP 65001
CHCP 65001
echo 需要移动的旧文件
current_path_old = f'{current_path}\*'
for /D %%a in ({current_path_old}) do (
set str1="%%a"
if "!str1!" == "!str1:{old_filename}=!" if "!str1!" == "!str1:config=!" if "!str1!" == "!str1:coreDirectory=!" if "!str1!" == "!str1:{old_filename}=!" (
echo 删除不包含old_file的文件夹: %%a
move /y "%%a" %{old_path}%
)
)
for %%i in ({current_path_old}) DO (
if not %%i=={bat_name} if not %%i=={old_filename}(
echo 删除文件:%%i
:: del /s /q "%%i"
move /y "%%i" %{old_path}%
)
)
for %%a in (*) do (
set str1=%%a
if "!str1!" == "!str1:{bat_name}=!" if "!str1!" == "!str1:{bat_name}=!" (
echo 删除不包含bat的文件: %%a
move /y "%%a" %{old_path}%
)
)
ping -n 2 127.0.0.1
xcopy /s /e /y /q "{new_file}\*" "{current_path}"
ping -n 2 127.0.0.1
if exist "{new_file_dir}" rd /s /q "{new_file_dir}"
echo 更新完成,等待自动启动{self.app_name}...
ping -n 2 127.0.0.1
"{new_file_exe}"
exit
'''))
updater.flush()
return bat_path
def auto_update_setup(self, filepath):
# 自动更新
bat_path = self.make_updater_bat(filepath)
subprocess.Popen(bat_path, encoding="gbk", shell=True)
def run(self):
# 检测更新,并返回跟新状态
version_info = self.get_version_info()
print('version_info1:', version_info)
self.is_down_update = False
if version_info and version_info.get("code") == 200:
if version_info['is_force_update'] == 0:
# 可选择更新
self.check_version(version_info)
else:
self.is_down_update = True
# 强制更新
self.down_zip(version_info)
if self.is_down_update:
os._exit(1)
return self.is_down_update
if __name__ == '__main__':
start = Unit_Test_Version()
start.run()
# -*- coding: utf-8 -*-
import time
from datetime import datetime
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from pydantic import BaseModel
import asyncio
from fastapi import FastAPI, File, UploadFile, Form
from link_sql import Link_Sql
from tools import log_count
from starlette.responses import FileResponse
"""
http://127.0.0.1:19315/search_version
post请求
参数:soft_type 软件类型
data={
"soft_type":1
}
返回结果:
{
'version': '1.0.2',
'zip_url': 'https://127.0.0.1:19315/zipfiles/xxxx.zip',
'file_name': '软件测试',
'remark': '版本测试', # 更新内容
'is_force_update': 0 # 0.可选择是否更新 1. 强制更新
}
"""
class Comment_Task():
def __init__(self):
self.loggings = log_count()
self.db = Link_Sql(self.loggings)
def search_version(self, item):
"""
查询版本信息
"""
soft_type = item.soft_type
sql = 'select version,zip_url,file_name,remark,is_force_update from plugin_version where soft_type=%s ORDER BY create_time desc'
result_data = self.db.one_search(sql % soft_type, to_json=True)
if result_data:
result_data["code"] = 200
else:
result_data["code"] = 500
print('result_data:', result_data)
return result_data
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 表示允许任何源
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class Update_Status(BaseModel):
soft_type: int
method_name_dic = {
# 查询版本信息
"search_version": Comment_Task().search_version,
}
@app.post('/search_version')
async def search_version(item: Update_Status):
method_name = "search_version"
try:
loop = asyncio.get_event_loop()
method_ = method_name_dic[method_name]
data = await loop.run_in_executor(None, method_, item)
return data
except:
return {"code": 500, "message": "查询失败"}
@app.get("/zipfiles/{file_path}/{file_name}")
async def download(file_path: str, file_name: str):
# 下载压缩包 zipfiles为在当前文件 建立的文件夹放置压缩包
filename = f"zipfiles/{file_path}/{file_name}"
print('filename:', filename)
return FileResponse(filename, filename=f"{file_name}") #展示的下载的文件名
# http://192.168.18.245:19315/zipfiles/软件测试/soft_test.zip
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=19315)