Updating and downloading of AppImages might work

This commit is contained in:
Dominik Demuth 2023-01-06 20:40:31 +01:00
parent bfe28f127d
commit 6eff9b5d91
2 changed files with 142 additions and 60 deletions

View File

@ -1,9 +1,13 @@
import sys from __future__ import annotations
from functools import lru_cache
from os import getenv, stat from os import getenv, stat
from os.path import exists
import hashlib import hashlib
import subprocess import subprocess
from datetime import datetime from datetime import datetime
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path
import requests import requests
from numpy import linspace from numpy import linspace
@ -58,9 +62,7 @@ class RdBuCMap:
class UpdateDialog(QtWidgets.QDialog): class UpdateDialog(QtWidgets.QDialog):
host = 'mirror.infra.pkm' startDownload = QtCore.pyqtSignal(list)
bucket = 'nmreval'
version = 'NMReval-latest-x86_64'
def __init__(self, filename: str = None, parent=None): def __init__(self, filename: str = None, parent=None):
super().__init__(parent=parent) super().__init__(parent=parent)
@ -69,11 +71,20 @@ class UpdateDialog(QtWidgets.QDialog):
if filename is None: if filename is None:
filename = getenv('APPIMAGE') filename = getenv('APPIMAGE')
self._appfile = filename self._appfile = filename
self._url_zsync = f'http://{self.host}/{self.bucket}/{self.version}.AppImage.zsync'
self.process = QtCore.QProcess(self) self.success = False
self.updater = Updater()
self.look_for_updates() self.thread = QtCore.QThread(self)
self.thread.start()
self.helper = Downloader()
self.startDownload.connect(self.helper.run_download)
self.helper.progressChanged.connect(self.status.setText)
self.helper.finished.connect(self.finish_update)
self.helper.started.connect(self.status.show)
self.helper.moveToThread(self.thread)
self.look_for_updates(self._appfile)
def _init_ui(self): def _init_ui(self):
self.setWindowTitle('Updates') self.setWindowTitle('Updates')
@ -85,50 +96,135 @@ class UpdateDialog(QtWidgets.QDialog):
layout.addSpacing(10) layout.addSpacing(10)
self.status = QtWidgets.QLabel()
self.status.hide()
layout.addWidget(self.status)
layout.addSpacing(10)
self.dialog_button = QtWidgets.QDialogButtonBox() self.dialog_button = QtWidgets.QDialogButtonBox()
self.dialog_button.accepted.connect(self.accept) self.dialog_button.accepted.connect(self.update_appimage)
self.dialog_button.rejected.connect(self.reject) self.dialog_button.rejected.connect(self.close)
layout.addWidget(self.dialog_button) layout.addWidget(self.dialog_button)
self.setLayout(layout) self.setLayout(layout)
def look_for_updates(self): def look_for_updates(self, filename=None):
# Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage # Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage
m_time_zsync, checksum_zsync = self.get_zsync() is_updateble, m_time_file, m_time_zsync = self.updater.get_update_information(filename)
m_time_file, checksum_file = self.get_appimage_info()
if m_time_zsync is None: if m_time_zsync is None:
label_text = '<p>Retrieval of version information failed.</p>' \ label_text = '<p>Retrieval of version information failed.</p>' \
'<p>Please try later (or complain to people that it does not work).</p>' '<p>Please try later (or complain to people that it does not work).</p>'
dialog_bttns = QtWidgets.QDialogButtonBox.Close dialog_bttns = QtWidgets.QDialogButtonBox.Close
else: else:
label_text = f'<p>Found most recent update: {m_time_zsync.strftime("%d %B %Y %H:%M")}</p>' label_text = f'<p>Date of most recent AppImage: {m_time_zsync.strftime("%d %B %Y %H:%M")}</p>'
if m_time_file is None: if m_time_file is None:
label_text += 'No AppImage file found, press Ok to downlaod latest version.' label_text += 'No AppImage file found, press Ok to download latest version.'
dialog_bttns = QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Close dialog_bttns = QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Close
else: else:
label_text += f'<p>Date of used AppImage: {m_time_file.strftime("%d %B %Y %H:%M")}</p>' label_text += f'<p>Date of used AppImage: {m_time_file.strftime("%d %B %Y %H:%M")}</p>'
if not ((checksum_file is not None) and (checksum_zsync is not None)): if is_updateble is None:
label_text += 'Could not determine if this version is newer, please update manually (if necessary).' self.status.setText('Could not determine if this version is newer, please update manually (if necessary).')
dialog_bttns = QtWidgets.QDialogButtonBox.Close dialog_bttns = QtWidgets.QDialogButtonBox.Close
elif checksum_file != checksum_zsync: elif is_updateble:
label_text += f'<p>Newer version available. <b>Update?</b></p>' self.status.setText(f'<p>Newer version available. Press Ok to download new version, Cancel to ignore.')
dialog_bttns = QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel dialog_bttns = QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel
else: else:
label_text += f'<p>Version is already the newest</p>' self.status.setText(f'Version may be already up-to-date.')
dialog_bttns = QtWidgets.QDialogButtonBox.Close dialog_bttns = QtWidgets.QDialogButtonBox.Close
self.label.setText(label_text) self.label.setText(label_text)
self.dialog_button.setStandardButtons(dialog_bttns) self.dialog_button.setStandardButtons(dialog_bttns)
def get_zsync(self): @QtCore.pyqtSlot()
def update_appimage(self):
if self._appfile is None:
args = [self.updater.zsync_url]
else:
# this breaks the download for some reason
args = ['-i', self._appfile, self.updater.zsync_url]
args = [self.updater.zsync_url]
self.dialog_button.setEnabled(False)
self.startDownload.emit(args)
self.status.show()
@QtCore.pyqtSlot(int)
def finish_update(self, retcode: int):
# print('finished with', retcode)
self.success = retcode == 0
if retcode == 0:
self.status.setText('Download complete.')
else:
self.status.setText(f'Download failed :( with return code {retcode}.')
self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
self.dialog_button.setEnabled(True)
def closeEvent(self, evt):
self.thread.quit()
self.thread.wait()
if self.success:
appname = self.updater.get_zsync()[2]
if self._appfile is not None:
appimage_path = appname
old_version = Path(self._appfile).rename(self._appfile+'.old')
appimage_path = Path(appimage_path).replace(self._appfile)
else:
appimage_path = Path().cwd() / appname
# rename to version-agnostic name
appimage_path = appimage_path.rename('NMReval.AppImage')
_ = QtWidgets.QMessageBox.information(self, 'Complete',
f'New AppImage available at<br>{appimage_path}')
super().closeEvent(evt)
class Downloader(QtCore.QObject):
started = QtCore.pyqtSignal()
finished = QtCore.pyqtSignal(int)
progressChanged = QtCore.pyqtSignal(str)
@QtCore.pyqtSlot(list)
def run_download(self, args: list[str]):
process = subprocess.Popen(['zsync'] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True)
while True:
nextline = process.stdout.readline().strip()
if nextline:
self.progressChanged.emit(nextline)
# line = process.stderr.readline().strip()
if process.poll() is not None:
break
self.finished.emit(process.returncode)
class Updater:
host = 'mirror.infra.pkm'
bucket = 'nmreval'
version = 'NMReval-latest-x86_64'
@property
def zsync_url(self):
return f'http://{Updater.host}/{Updater.bucket}/{Updater.version}.AppImage.zsync'
@staticmethod
@lru_cache(3)
def get_zsync():
url_zsync = f'http://{Updater.host}/{Updater.bucket}/{Updater.version}.AppImage.zsync'
m_time_zsync = None m_time_zsync = None
checksum_zsync = None checksum_zsync = None
zsync_file = None zsync_file = None
filename = None
try: try:
response = requests.get(self._url_zsync) response = requests.get(url_zsync)
if response.status_code == requests.codes['\o/']: if response.status_code == requests.codes['\o/']:
zsync_file = response.content zsync_file = response.content
except Exception as e: except Exception as e:
@ -142,49 +238,39 @@ class UpdateDialog(QtWidgets.QDialog):
m_time_zsync = datetime.strptime(str(val, encoding='utf-8'), '%a, %d %b %Y %H:%M:%S %z').astimezone(None) m_time_zsync = datetime.strptime(str(val, encoding='utf-8'), '%a, %d %b %Y %H:%M:%S %z').astimezone(None)
elif kw == b'SHA-1': elif kw == b'SHA-1':
checksum_zsync = str(val, encoding='utf-8') checksum_zsync = str(val, encoding='utf-8')
elif kw == b'Filename':
filename = str(val, encoding='utf-8')
except ValueError: except ValueError:
# stop when empty line is reached # stop when empty line is reached
break break
return m_time_zsync, checksum_zsync return m_time_zsync, checksum_zsync, filename
def get_appimage_info(self): @staticmethod
if self._appfile is None: def get_appimage_info(filename: str):
if filename is None:
return None, None return None, None
stat_mtime = stat(self._appfile).st_mtime if not exists(filename):
return None, None
stat_mtime = stat(filename).st_mtime
m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0) m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0)
with open(self._appfile, 'rb') as f: with open(filename, 'rb') as f:
checksum_file = hashlib.sha1(f.read()).hexdigest() checksum_file = hashlib.sha1(f.read()).hexdigest()
return m_time_file, checksum_file return m_time_file, checksum_file
def update_appimage(self): @staticmethod
if self._appfile is None: def get_update_information(filename: str) -> tuple[(bool | None), datetime, datetime]:
args = [self._url_zsync] m_time_zsync, checksum_zsync, appname = Updater.get_zsync()
m_time_file, checksum_file = Updater.get_appimage_info(filename)
if not ((checksum_file is not None) and (checksum_zsync is not None)):
return None, m_time_file, m_time_zsync
else: else:
args = ['-i', self._appfile, self._url_zsync] return checksum_file != checksum_zsync, m_time_file, m_time_zsync
self.process.readyReadStandardOutput.connect(self.onReadyReadStandardOutput)
self.process.readyReadStandardError.connect(self.onReadyReadStandardOutput)
self.process.start('zsync', args)
if not self.process.waitForFinished():
return False
return True
def onReadyReadStandardOutput(self):
result = self.process.readAllStandardOutput().data().decode()
self.label.setText(result)
def accept(self):
if self.update_appimage():
_ = QtWidgets.QMessageBox.information(self, 'Updates', 'Download finished. Execute AppImage for new version.')
super().accept()
def open_bug_report(): def open_bug_report():
@ -205,12 +291,3 @@ def open_bug_report():
import webbrowser import webbrowser
webbrowser.open(full_url) webbrowser.open(full_url)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = UpdateDialog()
w.show()
sys.exit(app.exec())

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os
import pathlib import pathlib
import re import re
from pathlib import Path from pathlib import Path
@ -27,7 +28,7 @@ from ..math.smooth import QSmooth
from ..nmr.coupling_calc import QCoupCalcDialog from ..nmr.coupling_calc import QCoupCalcDialog
from ..nmr.t1_from_tau import QRelaxCalc from ..nmr.t1_from_tau import QRelaxCalc
from .._py.basewindow import Ui_BaseWindow from .._py.basewindow import Ui_BaseWindow
from ..lib.utils import UpdateDialog, open_bug_report from ..lib.utils import UpdateDialog, open_bug_report, Updater
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@ -68,6 +69,10 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
self._init_gui() self._init_gui()
self._init_signals() self._init_signals()
if os.getenv('APPIMAGE') is not None:
if Updater.get_update_information(os.getenv('APPIMAGE'))[0]:
self.look_for_update()
def _init_gui(self): def _init_gui(self):
self.setupUi(self) self.setupUi(self)
make_action_icons(self) make_action_icons(self)