forked from IPKM/nmreval
		
	Updating and downloading of AppImages might work
This commit is contained in:
		@@ -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())
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user