From 0a2307d56f26c730abba20851eac2b746178856b Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 26 Feb 2018 09:52:33 -0600 Subject: [PATCH] Added updater at the start of the program --- src/application.py | 2 +- src/controller/mainController.py | 3 +- src/update/__init__.py | 0 src/update/update.py | 116 +++++++++++++++++++++++++++++++ src/update/updater.py | 14 ++++ src/update/utils.py | 42 +++++++++++ src/update/wxUpdater.py | 29 ++++++++ src/widgetUtils/__init__.pyc | Bin 223 -> 0 bytes src/widgetUtils/wxUtils.pyc | Bin 8718 -> 0 bytes 9 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 src/update/__init__.py create mode 100644 src/update/update.py create mode 100644 src/update/updater.py create mode 100644 src/update/utils.py create mode 100644 src/update/wxUpdater.py delete mode 100644 src/widgetUtils/__init__.pyc delete mode 100644 src/widgetUtils/wxUtils.pyc diff --git a/src/application.py b/src/application.py index ad8bb46..b51a79c 100644 --- a/src/application.py +++ b/src/application.py @@ -6,7 +6,7 @@ authorEmail = "manuel@manuelcortez.net" copyright = "Copyright (C) 2018, Manuel Cortez" description = name+" Is an application that will allow you to download music from popular sites such as youtube, zaycev.net." url = "https://manuelcortez.net/music_dl" -#update_url = "https://raw.githubusercontent.com/manuelcortez/socializer/master/update-files/socializer.json" +update_url = "https://manuelcortez.net/music_dl/update" # The short name will be used for detecting translation files. See languageHandler for more details. short_name = "musicdl" translators = [] \ No newline at end of file diff --git a/src/controller/mainController.py b/src/controller/mainController.py index c6192e1..129fadb 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -7,6 +7,7 @@ import utils from pubsub import pub from wxUI import mainWindow, menus from extractors import zaycev, youtube +from update import updater from . import player log = logging.getLogger("controller.main") @@ -18,7 +19,6 @@ class Controller(object): log.debug("Starting main controller...") # Setting up the player object player.setup() - # Instantiate the only available extractor for now. # Get main window self.window = mainWindow.mainWindow() log.debug("Main window created") @@ -31,6 +31,7 @@ class Controller(object): self.timer.Start(75) self.window.vol_slider.SetValue(player.player.volume) # Shows window. + utils.call_threaded(updater.do_update) self.window.Show() def get_status_info(self): diff --git a/src/update/__init__.py b/src/update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/update/update.py b/src/update/update.py new file mode 100644 index 0000000..84299dc --- /dev/null +++ b/src/update/update.py @@ -0,0 +1,116 @@ +from logging import getLogger +logger = getLogger('update') + +import contextlib +import io +import os +import platform +import requests +import tempfile +try: + import czipfile as zipfile +except ImportError: + import zipfile + +from platform_utils import paths + +def perform_update(endpoint, current_version, app_name='', password=None, update_available_callback=None, progress_callback=None, update_complete_callback=None): + requests_session = create_requests_session(app_name=app_name, version=current_version) + available_update = find_update(endpoint, requests_session=requests_session) + if not available_update: + logger.debug("No update available") + return False + available_version = float(available_update['current_version']) + if not float(available_version) > float(current_version) or platform.system()+platform.architecture()[0][:2] not in available_update['downloads']: + logger.debug("No update for this architecture") + return False + available_description = available_update.get('description', None) + update_url = available_update ['downloads'][platform.system()+platform.architecture()[0][:2]] + logger.info("A new update is available. Version %s" % available_version) + if callable(update_available_callback) and not update_available_callback(version=available_version, description=available_description): #update_available_callback should return a falsy value to stop the process + logger.info("User canceled update.") + return + base_path = tempfile.mkdtemp() + download_path = os.path.join(base_path, 'update.zip') + update_path = os.path.join(base_path, 'update') + downloaded = download_update(update_url, download_path, requests_session=requests_session, progress_callback=progress_callback) + extracted = extract_update(downloaded, update_path, password=password) + bootstrap_path = move_bootstrap(extracted) + execute_bootstrap(bootstrap_path, extracted) + logger.info("Update prepared for installation.") + if callable(update_complete_callback): + update_complete_callback() + +def create_requests_session(app_name=None, version=None): + user_agent = '' + session = requests.session() + if app_name: + user_agent = ' %s/%r' % (app_name, version) + session.headers['User-Agent'] = session.headers['User-Agent'] + user_agent + return session + +def find_update(endpoint, requests_session): + response = requests_session.get(endpoint) + response.raise_for_status() + content = response.json() + return content + +def download_update(update_url, update_destination, requests_session, progress_callback=None, chunk_size=io.DEFAULT_BUFFER_SIZE): + total_downloaded = total_size = 0 + with io.open(update_destination, 'w+b') as outfile: + download = requests_session.get(update_url, stream=True) + total_size = int(download.headers.get('content-length', 0)) + logger.debug("Total update size: %d" % total_size) + download.raise_for_status() + for chunk in download.iter_content(chunk_size): + outfile.write(chunk) + total_downloaded += len(chunk) + if callable(progress_callback): + call_callback(progress_callback, total_downloaded, total_size) + logger.debug("Update downloaded") + return update_destination + +def extract_update(update_archive, destination, password=None): + """Given an update archive, extracts it. Returns the directory to which it has been extracted""" + with contextlib.closing(zipfile.ZipFile(update_archive)) as archive: + if password: + archive.setpassword(password) + archive.extractall(path=destination) + logger.debug("Update extracted") + return destination + +def move_bootstrap(extracted_path): + working_path = os.path.abspath(os.path.join(extracted_path, '..')) + if platform.system() == 'Darwin': + extracted_path = os.path.join(extracted_path, 'Contents', 'Resources') + downloaded_bootstrap = os.path.join(extracted_path, bootstrap_name()) + new_bootstrap_path = os.path.join(working_path, bootstrap_name()) + os.rename(downloaded_bootstrap, new_bootstrap_path) + return new_bootstrap_path + +def execute_bootstrap(bootstrap_path, source_path): + arguments = r'"%s" "%s" "%s" "%s"' % (os.getpid(), source_path, paths.app_path(), paths.get_executable()) + if platform.system() == 'Windows': + import win32api + win32api.ShellExecute(0, 'open', bootstrap_path, arguments, '', 5) + else: + import subprocess + make_executable(bootstrap_path) + subprocess.Popen(['%s %s' % (bootstrap_path, arguments)], shell=True) + logger.info("Bootstrap executed") + +def bootstrap_name(): + if platform.system() == 'Windows': return 'bootstrap.exe' + if platform.system() == 'Darwin': return 'bootstrap-mac.sh' + return 'bootstrap-lin.sh' + +def make_executable(path): + import stat + st = os.stat(path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + +def call_callback(callback, *args, **kwargs): +# try: + callback(*args, **kwargs) +# except: +# logger.exception("Failed calling callback %r with args %r and kwargs %r" % (callback, args, kwargs)) diff --git a/src/update/updater.py b/src/update/updater.py new file mode 100644 index 0000000..1627d21 --- /dev/null +++ b/src/update/updater.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import application +import platform +import logging +from requests.exceptions import ConnectionError +from .import update +from .wxUpdater import * +logger = logging.getLogger("updater") + +def do_update(endpoint=application.update_url): + try: + return update.perform_update(endpoint=endpoint, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished) + except ConnectionError: + logger.exception("Update failed.") \ No newline at end of file diff --git a/src/update/utils.py b/src/update/utils.py new file mode 100644 index 0000000..d681107 --- /dev/null +++ b/src/update/utils.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +def convert_bytes(n): + K, M, G, T, P = 1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50 + if n >= P: + return '%.2fPb' % (float(n) / T) + elif n >= T: + return '%.2fTb' % (float(n) / T) + elif n >= G: + return '%.2fGb' % (float(n) / G) + elif n >= M: + return '%.2fMb' % (float(n) / M) + elif n >= K: + return '%.2fKb' % (float(n) / K) + else: + return '%d' % n + +def seconds_to_string(seconds, precision=0): + day = seconds // 86400 + hour = seconds // 3600 + min = (seconds // 60) % 60 + sec = seconds - (hour * 3600) - (min * 60) + sec_spec = "." + str(precision) + "f" + sec_string = sec.__format__(sec_spec) + string = "" + if day == 1: + string += _(u"%d day, ") % day + elif day >= 2: + string += _(u"%d days, ") % day + if (hour == 1): + string += _(u"%d hour, ") % hour + elif (hour >= 2): + string += _("%d hours, ") % hour + if (min == 1): + string += _(u"%d minute, ") % min + elif (min >= 2): + string += _(u"%d minutes, ") % min + if sec >= 0 and sec <= 2: + string += _(u"%s second") % sec_string + else: + string += _(u"%s seconds") % sec_string + return string \ No newline at end of file diff --git a/src/update/wxUpdater.py b/src/update/wxUpdater.py new file mode 100644 index 0000000..1e3f809 --- /dev/null +++ b/src/update/wxUpdater.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import wx +import application +import utils + +progress_dialog = None + +def available_update_dialog(version, description): + dialog = wx.MessageDialog(None, _(u"There's a new %s version available. Would you like to download it now?\n\n %s version: %s\n\nChanges:\n%s") % (application.name, application.name, version, description), _(u"New version for %s") % application.name, style=wx.YES|wx.NO|wx.ICON_WARNING) + if dialog.ShowModal() == wx.ID_YES: + return True + else: + return False + +def create_progress_dialog(): + return wx.ProgressDialog(_(u"Download in Progress"), _(u"Downloading the new version..."), parent=None, maximum=100) + +def progress_callback(total_downloaded, total_size): + global progress_dialog + if progress_dialog == None: + progress_dialog = create_progress_dialog() + progress_dialog.Show() + if total_downloaded == total_size: + progress_dialog.Destroy() + else: + progress_dialog.Update((total_downloaded*100)/total_size, _(u"Updating... %s of %s") % (str(utils.convert_bytes(total_downloaded)), str(utils.convert_bytes(total_size)))) + +def update_finished(): + ms = wx.MessageDialog(None, _(u"The update has been downloaded and installed successfully. Press OK to continue."), _(u"Done!")).ShowModal() \ No newline at end of file diff --git a/src/widgetUtils/__init__.pyc b/src/widgetUtils/__init__.pyc deleted file mode 100644 index 460139a4141b9c243fe5e35acfd8c83648f547eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmYLDOA5k344t+MMf3umU~u7DL_|=iD>t?aB~WTR%D}W!CMi{~?Ey4X1s}=FOF}-; zYcg6S;#7zqLXI1DoHCVcfiOaxzd95q5DAx#GUB#=GY&FfNuFlXPt%a8F*#kWt7yPU zcTY(hlq|)cFy?f-gcOxSsPej126nb+D^(LhtZCm~0Bxajqo2wFx&k^~-W2ugAj@>E XDa+=~qrbVTY}p#M_OGD-WE_3~+*dV} diff --git a/src/widgetUtils/wxUtils.pyc b/src/widgetUtils/wxUtils.pyc deleted file mode 100644 index 80fdee1af34dd2bff5388c425dbce2ac04f42e27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8718 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