summaryrefslogtreecommitdiff
path: root/upgrade.py
diff options
context:
space:
mode:
Diffstat (limited to 'upgrade.py')
-rw-r--r--upgrade.py309
1 files changed, 309 insertions, 0 deletions
diff --git a/upgrade.py b/upgrade.py
new file mode 100644
index 0000000..d952608
--- /dev/null
+++ b/upgrade.py
@@ -0,0 +1,309 @@
+# Copyright (C) 2017 Damon Lynch <damonlynch@gmail.com>
+
+# This file is part of Rapid Photo Downloader.
+#
+# Rapid Photo Downloader is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Rapid Photo Downloader is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Rapid Photo Downloader. If not,
+# see <http://www.gnu.org/licenses/>.
+
+"""
+Helper program to upgrade Rapid Photo Downloader using pip
+"""
+
+__author__ = 'Damon Lynch'
+__copyright__ = "Copyright 2017, Damon Lynch"
+
+
+import sys
+import os
+import tarfile
+import tempfile
+import shutil
+import re
+from typing import List, Optional
+import shlex
+from subprocess import Popen, PIPE
+from queue import Queue, Empty
+import subprocess
+from gettext import gettext as _
+
+from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QThread, QObject, QTimer)
+from PyQt5.QtGui import QIcon, QFontMetrics, QFont, QFontDatabase
+from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QVBoxLayout, QTextEdit,
+ QDialogButtonBox, QStackedWidget, QLabel)
+from PyQt5.QtNetwork import QLocalSocket
+
+import raphodo.qrc_resources as qrc_resources
+
+
+q = Queue()
+
+
+class RPDUpgrade(QObject):
+ """
+ Upgrade Rapid Photo Downloader using python's pip
+ """
+
+ message = pyqtSignal(str)
+ upgradeFinished = pyqtSignal(bool)
+
+
+ def make_pip_command(self, args: str) -> List[str]:
+ return shlex.split('{} -m pip {}'.format(sys.executable, args))
+
+ @pyqtSlot(str)
+ def start(self, installer: str) -> None:
+
+ name = os.path.basename(installer)
+ name = name[:len('.tar.gz') * -1]
+
+ rpath = os.path.join(name, 'requirements.txt')
+ try:
+ with tarfile.open(installer) as tar:
+ with tar.extractfile(rpath) as requirements:
+ reqbytes = requirements.read()
+ with tempfile.NamedTemporaryFile(delete=False) as temp_requirements:
+ temp_requirements.write(reqbytes)
+ temp_requirements_name = temp_requirements.name
+ except Exception:
+ self.failure("Failed to extract application requirements")
+ return
+
+ self.sendMessage("Installing application requirements...\n")
+ try:
+ cmd = self.make_pip_command('install --user -r {}'.format(temp_requirements.name))
+ with Popen(cmd, stdout=PIPE, stderr=PIPE, bufsize=1, universal_newlines=True) as p:
+ for line in p.stdout:
+ self.sendMessage(line, truncate=True)
+ cmd = self.checkForCmd()
+ if cmd is not None:
+ assert cmd == 'STOP'
+ self.failure('\nTermination requested')
+ return
+ p.wait()
+ i = p.returncode
+ os.remove(temp_requirements_name)
+ if i != 0:
+ self.failure("Failed to install application requirements: %i" % i)
+ return
+ except Exception:
+ self.sendMessage(sys.exc_info())
+ self.failure("Failed to install application requirements")
+ return
+
+ self.sendMessage("\nInstalling application...\n")
+ try:
+ cmd = self.make_pip_command('install --user --no-deps {}'.format(installer))
+ with Popen(cmd, stdout=PIPE, stderr=PIPE, bufsize=1, universal_newlines=True) as p:
+ for line in p.stdout:
+ self.sendMessage(line, truncate=True)
+ cmd = self.checkForCmd()
+ if cmd is not None:
+ assert cmd == 'STOP'
+ self.failure('\nTermination requested')
+ return
+ p.wait()
+ i = p.returncode
+ if i != 0:
+ self.failure("Failed to install application")
+ return
+ except Exception:
+ self.failure("Failed to install application")
+ return
+
+ self.upgradeFinished.emit(True)
+
+ def failure(self, message: str) -> None:
+ self.sendMessage(message)
+ self.upgradeFinished.emit(False)
+
+
+ def sendMessage(self, message: str, truncate=False) -> None:
+ if truncate:
+ self.message.emit(message[:-1])
+ else:
+ self.message.emit(message)
+
+ def checkForCmd(self) -> Optional[str]:
+ try:
+ return q.get(block=False)
+ except Empty:
+ return None
+
+
+def extract_version_number(installer: str) -> str:
+ targz = os.path.basename(installer)
+ parsed_version = targz[:targz.find('tar') - 1]
+
+ first_digit = re.search("\d", parsed_version)
+ return parsed_version[first_digit.start():]
+
+
+class UpgradeDialog(QDialog):
+ """
+ Very simple dialog window that allows user to initiate
+ Rapid Photo Downloader upgrade and shows output of that
+ upgrade.
+ """
+
+ startUpgrade = pyqtSignal(str)
+ def __init__(self, installer):
+ super().__init__()
+
+ self.installer = installer
+ self.setWindowTitle(_('Upgrade Rapid Photo Downloader'))
+
+ try:
+ self.version_no = extract_version_number(installer=installer)
+ except Exception:
+ self.version_no = ''
+
+ self.running = False
+
+ self.textEdit = QTextEdit()
+ self.textEdit.setReadOnly(True)
+
+ fixed = QFontDatabase.systemFont(QFontDatabase.FixedFont) # type: QFont
+ fixed.setPointSize(fixed.pointSize() - 1)
+ self.textEdit.setFont(fixed)
+
+ font_height = QFontMetrics(fixed).height()
+
+ height = font_height * 20
+
+ width = QFontMetrics(fixed).boundingRect('a' * 90).width()
+
+ self.textEdit.setMinimumSize(width, height)
+
+ upgradeButtonBox = QDialogButtonBox(QDialogButtonBox.Cancel)
+ upgradeButtonBox.rejected.connect(self.reject)
+ upgradeButtonBox.accepted.connect(self.doUpgrade)
+ self.startButton = upgradeButtonBox.addButton(_('&Upgrade'),
+ QDialogButtonBox.AcceptRole) # QPushButton
+ # self.startButton.setDefault(True)
+
+ if self.version_no:
+ self.explanation = QLabel(_('Click the Upgrade button to upgrade to '
+ 'version %s.') % self.version_no)
+ else:
+ self.explanation = QLabel(_('Click the Upgrade button to start the upgrade.'))
+
+ finishButtonBox = QDialogButtonBox(QDialogButtonBox.Close)
+ finishButtonBox.addButton(_('&Run'), QDialogButtonBox.AcceptRole)
+ finishButtonBox.rejected.connect(self.reject)
+ finishButtonBox.accepted.connect(self.runNewVersion)
+
+ failedButtonBox = QDialogButtonBox(QDialogButtonBox.Close)
+ failedButtonBox.rejected.connect(self.reject)
+
+ self.stackedButtons = QStackedWidget()
+ self.stackedButtons.addWidget(upgradeButtonBox)
+ self.stackedButtons.addWidget(finishButtonBox)
+ self.stackedButtons.addWidget(failedButtonBox)
+
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+ layout.addWidget(self.textEdit)
+ layout.addWidget(self.explanation)
+ layout.addWidget(self.stackedButtons)
+
+ self.upgrade = RPDUpgrade()
+ self.upgradeThread = QThread()
+ self.startUpgrade.connect(self.upgrade.start)
+ self.upgrade.message.connect(self.appendText)
+ self.upgrade.upgradeFinished.connect(self.upgradeFinished)
+ self.upgrade.moveToThread(self.upgradeThread)
+ QTimer.singleShot(0, self.upgradeThread.start)
+
+ @pyqtSlot()
+ def doUpgrade(self) -> None:
+ if self.rpdRunning():
+ self.explanation.setText(_('Close Rapid Photo Downloader before running this upgrade'))
+ else:
+ self.running = True
+ self.explanation.setText(_('Upgrade running...'))
+ self.startButton.setEnabled(False)
+ self.startUpgrade.emit(self.installer)
+
+ def rpdRunning(self) -> bool:
+ """
+ Check to see if Rapid Photo Downloader is running
+ :return: True if it is
+ """
+
+ # keep next value in sync with value in raphodo/rapid.py
+ # can't import it
+ appGuid = '8dbfb490-b20f-49d3-9b7d-2016012d2aa8'
+ outSocket = QLocalSocket() # type: QLocalSocket
+ outSocket.connectToServer(appGuid)
+ isRunning = outSocket.waitForConnected() # type: bool
+ if outSocket:
+ outSocket.disconnectFromServer()
+ return isRunning
+
+ @pyqtSlot(str)
+ def appendText(self,text: str) -> None:
+ self.textEdit.append(text)
+
+ @pyqtSlot(bool)
+ def upgradeFinished(self, success: bool) -> None:
+ self.running = False
+
+ if success:
+ self.stackedButtons.setCurrentIndex(1)
+ else:
+ self.stackedButtons.setCurrentIndex(2)
+
+ if success:
+ if self.version_no:
+ message = _('Successfully upgraded to %s. Click Close to exit, or Run to '
+ 'start the program.' % self.version_no)
+ else:
+ message = _('Upgrade finished successfully. Click Close to exit, or Run to '
+ 'start the program.')
+ else:
+ message = _('Upgrade failed. Click Close to exit.')
+
+ self.explanation.setText(message)
+ self.deleteTar()
+
+ def deleteTar(self) -> None:
+ temp_dir = os.path.dirname(self.installer)
+ if temp_dir:
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+ def closeEvent(self, event) -> None:
+ self.upgradeThread.quit()
+ self.upgradeThread.wait()
+ event.accept()
+
+ @pyqtSlot()
+ def reject(self) -> None:
+ if self.running:
+ # strangely, using zmq in this program causes a segfault :-/
+ q.put('STOP')
+ super().reject()
+
+ @pyqtSlot()
+ def runNewVersion(self) -> None:
+ cmd = shutil.which('rapid-photo-downloader')
+ subprocess.Popen(cmd)
+ super().accept()
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ app.setWindowIcon(QIcon(':/rapid-photo-downloader.svg'))
+ widget = UpgradeDialog(sys.argv[1])
+ widget.show()
+ sys.exit(app.exec_()) \ No newline at end of file