Updating and downloading of AppImages might work
This commit is contained in:
parent
bfe28f127d
commit
6eff9b5d91
@ -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())
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user