本地客户端程序
py
import json
import hashlib
import os
import shutil
import requests
from pathlib import Path
class AutoUpdater:
def __init__(self, config_path="http://【XXXIP地址】/update_config"):
self.config_path = config_path
self.config = None
with open('update.json', 'r', encoding='utf-8') as f:
self.current_version = json.load(f)['version']
def load_config(self):
"""加载更新配置文件"""
try:
# 远程配置文件
response = requests.get(self.config_path)
self.config = response.json()
return True
except Exception as e:
print(f"加载配置文件失败: {str(e)}")
return False
def check_update(self):
"""检查是否需要更新"""
if not self.config:
return False
return self.config['version'] != self.current_version
def calculate_file_hash(self, file_path):
"""计算文件的MD5哈希值"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def download_update(self, temp_dir="temp_update"):
"""下载更新文件"""
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
try:
# 下载更新文件
update_file = f"{temp_dir}/update.zip"
response = requests.get('http://【XXXIP地址】/download/remote.zip', stream=True)
with open(update_file, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# 验证文件哈希
if self.calculate_file_hash(update_file) != self.config['file_hash']:
print("文件验证失败!")
return False
return update_file
except Exception as e:
print(f"下载更新失败: {str(e)}")
return False
def backup_files(self, backup_dir="backup"):
"""备份当前文件"""
try:
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
# 备份当前目录下的所有文件
for item in os.listdir('.'):
if item not in [backup_dir, 'temp_update', 'updater.exe','launcher.exe']:
src = os.path.join('.', item)
dst = os.path.join(backup_dir, item)
if os.path.isfile(src):
shutil.copy2(src, dst)
elif os.path.isdir(src):
shutil.copytree(src, dst)
return True
except Exception as e:
print(f"备份文件失败: {str(e)}")
return False
def apply_update(self, update_file, temp_dir="temp_update"):
"""应用更新"""
try:
# 解压更新文件
shutil.unpack_archive(update_file, temp_dir)
# 保留指定的旧文件和文件夹
keep_files = self.config.get('keep_files', [])
for file in keep_files:
if os.path.exists(file):
# 如果文件在子文件夹中,需要创建对应的临时文件夹
temp_path = os.path.join(temp_dir, f"{file}")
temp_dir_path = os.path.dirname(temp_path)
if temp_dir_path and not os.path.exists(temp_dir_path):
os.makedirs(temp_dir_path)
print(f"备份: {file} -> {temp_path}")
if os.path.isfile(file):
shutil.copy2(file, temp_path)
elif os.path.isdir(file):
# 如果是文件夹则整个复制
shutil.copytree(file, temp_path)
need_restart = False
# 修改删除文件的部分,添加重试和错误处理
for item in os.listdir('.'):
if item not in ['backup', 'temp_update', 'updater.exe','launcher.exe']:
path = os.path.join('.', item)
try:
if os.path.isfile(path):
try:
os.remove(path)
except PermissionError:
print(f"警告: 文件 {path} 正在使用中,将在重启后更新")
need_restart = True
continue
elif os.path.isdir(path):
try:
shutil.rmtree(path)
except PermissionError:
print(f"警告: 目录 {path} 中的某些文件正在使用中,将在重启后更新")
need_restart = True
continue
except Exception as e:
print(f"删除 {path} 时出错: {str(e)}")
continue
# 如果需要重启,创建标记文件
if need_restart:
with open("need_restart", "w") as f:
f.write("1")
print("部分文件需要在重启后完成更新")
return True # 仍然返回True,因为这是预期的行为
# 复制新文件时添加错误处理
for item in os.listdir(temp_dir):
src = os.path.join(temp_dir, item)
dst = os.path.join('.', item)
try:
if os.path.isfile(src):
try:
shutil.copy2(src, dst)
except PermissionError:
print(f"警告: 无法复制文件 {dst},可能需要重启后完成更新")
elif os.path.isdir(src):
try:
shutil.copytree(src, dst, dirs_exist_ok=True)
except PermissionError:
print(f"警告: 无法复制目录 {dst},可能需要重启后完成更新")
except Exception as e:
print(f"复制 {src} 到 {dst} 时出错: {str(e)}")
continue
# 恢复需要保留的旧文件和文件夹
for file in keep_files:
temp_path = os.path.join(temp_dir, file)
if os.path.exists(temp_path):
# 如果是带路径的文件,先创建目录
file_dir = os.path.dirname(file)
if file_dir and not os.path.exists(file_dir):
os.makedirs(file_dir)
if os.path.isfile(temp_path):
shutil.copy2(temp_path, file)
elif os.path.isdir(temp_path):
# 如果是文件夹则整个恢复
if os.path.exists(file):
shutil.rmtree(file)
shutil.copytree(temp_path, file)
return True
except Exception as e:
print(f"应用更新失败: {str(e)}, 错误位置: {e.__traceback__.tb_lineno}行")
return False
def cleanup(self, temp_dir="temp_update", backup_dir="backup"):
"""清理临时文件和备份"""
try:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
if os.path.exists(backup_dir):
shutil.rmtree(backup_dir)
if os.path.exists("update.zip"):
os.remove("update.zip")
except Exception as e:
print(f"清理文件失败: {str(e)}")
def update(self):
"""执行更新流程"""
if not self.load_config():
return False
if not self.check_update():
print("当前已是最新版本")
return True
print(f"发现新版本: {self.config['version']}")
print("更新说明:")
print(self.config['publish_notes'])
# 下载更新
update_file = self.download_update()
if not update_file:
return False
print(f"下载更新文件: {update_file}")
# 备份当前文件
if not self.backup_files():
return False
print("备份完成")
# 应用更新
if not self.apply_update(update_file):
print("更新失败,正在恢复备份...")
if self.apply_update("backup"):
print("恢复备份成功")
self.cleanup()
else:
print("恢复备份失败,请手动恢复")
return False
# 清理临时文件和备份
self.cleanup()
print(f"更新完成!当前版本: {self.config['version']}")
return True
if __name__ == "__main__":
try:
updater = AutoUpdater()
update_result = updater.update()
print("更新" + ("成功" if update_result else "失败"))
except Exception as e:
print(f"更新过程中发生错误: {str(e)}")
finally:
input("按任意键退出...")
启动器代码
py
import os
import sys
import subprocess
import time
import shutil
def main():
# 检查是否有更新程序在运行
updater_path = "updater.exe"
if os.path.exists(updater_path):
# 运行更新程序
subprocess.run([updater_path], check=True)
# 如果更新成功且需要重启
if os.path.exists("need_restart"):
# 删除重启标记
os.remove("need_restart")
# 等待原程序完全退出
time.sleep(2)
# 完成未完成的更新操作
if os.path.exists("temp_update"):
for root, dirs, files in os.walk("temp_update"):
for file in files:
src = os.path.join(root, file)
dst = os.path.join(root.replace("temp_update", ""), file)
try:
if os.path.exists(dst):
os.remove(dst)
shutil.copy2(src, dst)
except Exception as e:
print(f"更新文件失败: {e}")
# 清理临时文件
shutil.rmtree("temp_update")
# 启动主程序
main_program = "main.exe" # 替换为你的主程序名
if os.path.exists(main_program):
subprocess.run([main_program])
if __name__ == "__main__":
main()
用于更新的配置文件
json
{
"keep_files": [
"data.txt",
"updater.py"
],
"version": "1.0.0",
"publish_notes": "初始版本"
}
服务器flask程序
python
# app.py
from flask import Flask, jsonify, send_file, request
import os
import json
import hashlib
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 配置文件存储路径
UPLOAD_FOLDER = 'updates'
CONFIG_FILE = 'update_config.json'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def ensure_upload_folder():
"""确保上传文件夹存在"""
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
def calculate_file_hash(file_path):
"""计算文件MD5哈希值"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def update_config(version, notes, filename):
"""更新配置文件"""
config = {
"version": version,
"publish_notes": notes,
"download_url": f"/download/{filename}",
"file_hash": calculate_file_hash(os.path.join(app.config['UPLOAD_FOLDER'], filename)),
"keep_old_files": ["data.txt"] # 可以根据需要修改
}
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=4)
@app.route('/update_config')
def get_update_config():
"""获取更新配置"""
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return jsonify(json.load(f))
except FileNotFoundError:
return jsonify({"error": "Configuration file not found"}), 404
@app.route('/download/<filename>')
def download_file(filename):
"""下载更新文件"""
try:
return send_file(
os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(filename)),
as_attachment=True
)
except FileNotFoundError:
return jsonify({"error": "File not found"}), 404
@app.route('/upload', methods=['POST'])
def upload_file():
"""上传新版本"""
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
version = request.form.get('version')
notes = request.form.get('notes')
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
if file:
ensure_upload_folder()
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
update_config(version, notes, filename)
return jsonify({"message": "Upload successful"}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
# requirements.txt
"""
Flask==3.0.0
gunicorn==21.2.0
Werkzeug==3.0.0
"""
如何上传待更新的文件到服务器
bash
curl -X POST -F "zip_file=@remote.zip" -F "config=@keep_files.json" [your IP or domain]/upload
nginx配置
vim /etc/nginx/sites-available/update-server
python
server {
listen 80;
server_name XXXX; # 填写自己的域名或者ip地址
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
创建符号链接
sudo ln -s /etc/nginx/sites-available/update-server /etc/nginx/sites-enabled/
查看符号链接是否创建成功
ls -l /etc/nginx/sites-enabled/
测试连接并重启服务sudo nginx -t
sudo systemctl restart nginx