-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathupdater.py
137 lines (123 loc) · 7.21 KB
/
updater.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import atexit
import os
import sys
import zipfile
from io import BytesIO
from PyQt5.QtCore import Qt, QEventLoop, QStandardPaths, QUrl
from PyQt5.QtGui import QFont
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QApplication, QLabel, QMessageBox, QProgressBar, QPushButton, QVBoxLayout, QWidget
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
class _Application(QApplication):
def __init__(self, args):
super().__init__(args)
if sys.platform.startswith('win'): # qt5 uses deprecated windows API to determine the system font, this works around that issuue
self.setFont(QApplication.font("QMessageBox"))
class _UpdaterWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
self.setMinimumWidth(300)
layout = QVBoxLayout()
titleLabel = QLabel('Updating ZebraZoom...')
titleLabel.setFont(QFont('Times New Roman', 16))
layout.addWidget(titleLabel, alignment=Qt.AlignmentFlag.AlignCenter)
progressLabel = QLabel()
layout.addWidget(progressLabel)
progressBar = QProgressBar()
layout.addWidget(progressBar)
self.updateState = lambda state, total, text, showBtn=False: progressBar.setMaximum(total) or progressBar.setValue(state) or progressLabel.setText(text) or cancelDownloadBtn.setVisible(showBtn)
cancelDownloadBtn = QPushButton("Cancel download")
cancelDownloadBtn.clicked.connect(lambda: sys.exit(0))
layout.addWidget(cancelDownloadBtn, alignment=Qt.AlignmentFlag.AlignHCenter)
self.setLayout(layout)
class _ZipFile(zipfile.ZipFile): # this is required to work around a bug in ZipFile (https://github.com/python/cpython/issues/59999)
def _extract_member(self, member, targetpath, pwd):
if not isinstance(member, zipfile.ZipInfo):
member = self.getinfo(member)
path = super()._extract_member(member, targetpath, pwd)
if member.external_attr > 0xffff:
os.chmod(path, member.external_attr >> 16)
return path
if __name__ == '__main__' and getattr(sys, 'frozen', False): # running an installed executable
zebrazoomExecutable = 'ZebraZoom.exe' if sys.platform.startswith('win') else 'ZebraZoomApp' if sys.platform == 'darwin' else "ZebraZoom"
atexit.register(os.execl, zebrazoomExecutable, zebrazoomExecutable)
installationFolder = os.path.dirname(os.path.dirname(sys.executable))
legacyFormat = os.path.exists(os.path.join(installationFolder, zebrazoomExecutable))
atexit.register(os.chdir, installationFolder if legacyFormat else os.path.dirname(installationFolder))
app = _Application(sys.argv)
loop = QEventLoop()
networkManager = QNetworkAccessManager()
if legacyFormat:
assetName = f"ZebraZoom-{'update-Windows' if sys.platform.startswith('win') else 'update-macOS' if sys.platform == 'darwin' else 'Linux'}.zip"
else:
assetName = f"ZebraZoom-{'Windows' if sys.platform.startswith('win') else 'macOS' if sys.platform == 'darwin' else 'Linux'}.zip"
window = _UpdaterWindow()
window.show()
rect = window.geometry()
rect.moveCenter(window.screen().availableGeometry().center())
window.setGeometry(rect)
downloadRequest = QNetworkRequest(QUrl(f'https://github.com/oliviermirat/ZebraZoom/releases/latest/download/{assetName}'))
downloadRequest.setAttribute(QNetworkRequest.Attribute.RedirectPolicyAttribute, QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy)
download = networkManager.get(downloadRequest)
def downloadProgress(bytesReceived, bytesTotal):
if bytesTotal < 0:
return
window.updateState(bytesReceived, bytesTotal, 'Downloading update...', showBtn=True)
download.downloadProgress.connect(downloadProgress)
download.finished.connect(lambda: loop.exit())
loop.exec()
if download.error() != QNetworkReply.NetworkError.NoError:
QMessageBox.critical(window, "Download failed", "Could not download the update, please try again or update manually.")
sys.exit(0)
try:
with open(os.path.join(installationFolder, 'installedFiles.txt')) as f:
installedFiles = f.read().splitlines()
if not legacyFormat:
installedFiles.append(os.path.join('..', zebrazoomExecutable))
except EnvironmentError:
QMessageBox.critical(window, "Error deleting old files", "Could not delete some of the old files. Installation might have been damaged, please reinstall ZebraZoom manually.")
sys.exit(0)
dataInDocuments = os.path.exists(os.path.join(installationFolder, 'Uninstall.exe'))
documentsFolder = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DocumentsLocation)
processedSize = 0
totalSize = len(installedFiles)
try:
for fname in installedFiles:
if fname.startswith('updater'):
continue
if dataInDocuments and any(fname.startswith(folder) for folder in ('zebrazoom\configuration', 'zebrazoom\dataAnalysis', 'zebrazoom\ZZoutput')):
path = os.path.join(documentsFolder, 'ZebraZoom', fname.lstrip('zebrazoom\\'))
else:
path = os.path.join(installationFolder, fname)
window.updateState(processedSize, totalSize, 'Removing old files...')
processedSize += 1
if os.path.isdir(path):
try:
os.rmdir(path)
except OSError:
continue # directory is not empty; skip it
elif os.path.exists(path):
os.remove(path)
except EnvironmentError:
QMessageBox.critical(window, "Error deleting old files", "Could not delete some of the old files. Installation might have been damaged, please reinstall ZebraZoom manually.")
sys.exit(0)
try:
with _ZipFile(BytesIO(download.readAll()), 'r') as archive:
totalSize = sum(info.file_size for info in archive.infolist())
processedSize = 0
window.updateState(processedSize, totalSize, 'Extracting new files...')
for info in archive.infolist():
extractToFolder = installationFolder if legacyFormat else os.path.dirname(installationFolder)
if 'updater/updater' in info.filename:
info.filename += '.new'
elif dataInDocuments and any(info.filename.startswith(folder) for folder in ('zebrazoom/configuration', 'zebrazoom/dataAnalysis', 'zebrazoom/ZZoutput')):
info.filename = info.filename.lstrip('zebrazoom/')
extractToFolder = os.path.join(documentsFolder, 'ZebraZoom')
archive.extract(info, extractToFolder)
processedSize += info.file_size
window.updateState(processedSize, totalSize, 'Extracting new files...')
except: # we really don't care what goes wrong here
QMessageBox.critical(window, "Error extracting new files", "Could not extract some files. Installation might have been damaged, please reinstall ZebraZoom manually.")