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.path import exists
import hashlib
import subprocess
from datetime import datetime
from contextlib import contextmanager
from pathlib import Path
import requests
from numpy import linspace
@ -58,9 +62,7 @@ class RdBuCMap:
class UpdateDialog(QtWidgets.QDialog):
host = 'mirror.infra.pkm'
bucket = 'nmreval'
version = 'NMReval-latest-x86_64'
startDownload = QtCore.pyqtSignal(list)
def __init__(self, filename: str = None, parent=None):
super().__init__(parent=parent)
@ -69,11 +71,20 @@ class UpdateDialog(QtWidgets.QDialog):
if filename is None:
filename = getenv('APPIMAGE')
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):
self.setWindowTitle('Updates')
@ -85,50 +96,135 @@ class UpdateDialog(QtWidgets.QDialog):
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.accepted.connect(self.accept)
self.dialog_button.rejected.connect(self.reject)
self.dialog_button.accepted.connect(self.update_appimage)
self.dialog_button.rejected.connect(self.close)
layout.addWidget(self.dialog_button)
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
m_time_zsync, checksum_zsync = self.get_zsync()
m_time_file, checksum_file = self.get_appimage_info()
is_updateble, m_time_file, m_time_zsync = self.updater.get_update_information(filename)
if m_time_zsync is None:
label_text = '<p>Retrieval of version information failed.</p>' \
'<p>Please try later (or complain to people that it does not work).</p>'
dialog_bttns = QtWidgets.QDialogButtonBox.Close
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:
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
else:
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)):
label_text += 'Could not determine if this version is newer, please update manually (if necessary).'
if is_updateble is None:
self.status.setText('Could not determine if this version is newer, please update manually (if necessary).')
dialog_bttns = QtWidgets.QDialogButtonBox.Close
elif checksum_file != checksum_zsync:
label_text += f'<p>Newer version available. <b>Update?</b></p>'
elif is_updateble:
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
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
self.label.setText(label_text)
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
checksum_zsync = None
zsync_file = None
filename = None
try:
response = requests.get(self._url_zsync)
response = requests.get(url_zsync)
if response.status_code == requests.codes['\o/']:
zsync_file = response.content
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)
elif kw == b'SHA-1':
checksum_zsync = str(val, encoding='utf-8')
elif kw == b'Filename':
filename = str(val, encoding='utf-8')
except ValueError:
# stop when empty line is reached
break
return m_time_zsync, checksum_zsync
return m_time_zsync, checksum_zsync, filename
def get_appimage_info(self):
if self._appfile is None:
@staticmethod
def get_appimage_info(filename: str):
if filename is 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)
with open(self._appfile, 'rb') as f:
with open(filename, 'rb') as f:
checksum_file = hashlib.sha1(f.read()).hexdigest()
return m_time_file, checksum_file
def update_appimage(self):
if self._appfile is None:
args = [self._url_zsync]
@staticmethod
def get_update_information(filename: str) -> tuple[(bool | None), datetime, datetime]:
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:
args = ['-i', self._appfile, self._url_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()
return checksum_file != checksum_zsync, m_time_file, m_time_zsync
def open_bug_report():
@ -205,12 +291,3 @@ def open_bug_report():
import webbrowser
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
import os
import pathlib
import re
from pathlib import Path
@ -27,7 +28,7 @@ from ..math.smooth import QSmooth
from ..nmr.coupling_calc import QCoupCalcDialog
from ..nmr.t1_from_tau import QRelaxCalc
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):
@ -68,6 +69,10 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
self._init_gui()
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):
self.setupUi(self)
make_action_icons(self)