diff --git a/src/application.py b/src/application.py index d5c56ac..6041011 100644 --- a/src/application.py +++ b/src/application.py @@ -1,14 +1,11 @@ # -*- coding: utf-8 -*- name = "Socializer" -snapshot = True -if snapshot == False: - version = "0.1" -else: - version = "0.1" +version = "0.1" author = u"Manuel Cortéz" authorEmail = "manuel@manuelcortez.net" copyright = u"Copyright (C) 2016, Manuel cortéz." description = unicode(name+" Is an accessible VK client for Windows.") url = "https://github.com/manuelcortez/socializer" +update_url = "https://raw.githubusercontent.com/manuelcortez/socializer/master/update-files/socializer.json" # The short name will be used for detecting translation files. See languageHandler for more details. short_name = "socializer" \ No newline at end of file diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 4248bac..a94121a 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -9,7 +9,8 @@ from pubsub import pub from mysc.repeating_timer import RepeatingTimer from mysc.thread_utils import call_threaded from sessionmanager import session -from wxUI import (mainWindow) +from wxUI import (mainWindow, commonMessages) +from update import updater class Controller(object): @@ -36,6 +37,7 @@ class Controller(object): self.create_controls() self.window.Show() self.connect_events() + call_threaded(updater.do_update) def create_controls(self): home = buffers.baseBuffer(parent=self.window.tb, name="home_timeline", session=self.session, composefunc="compose_new", endpoint="newsfeed") @@ -57,6 +59,7 @@ class Controller(object): pub.subscribe(self.view_post, "open-post") widgetUtils.connect_event(self.window, widgetUtils.CLOSE_EVENT, self.exit) widgetUtils.connect_event(self.window, widgetUtils.MENU, self.update_buffer, menuitem=self.window.update_buffer) + widgetUtils.connect_event(self.window, widgetUtils.MENU, self.check_for_updates, menuitem=self.window.check_for_updates) def disconnect_events(self): pub.unsubscribe(self.in_post, "posted") @@ -105,3 +108,8 @@ class Controller(object): def update_buffer(self, *args, **kwargs): b = self.get_current_buffer() b.get_items() + + def check_for_updates(self, *args, **kwargs): + update = updater.do_update() + if update == False: + commonMessages.no_update_available() diff --git a/src/update/__init__.py b/src/update/__init__.py new file mode 100644 index 0000000..cff6566 --- /dev/null +++ b/src/update/__init__.py @@ -0,0 +1,12 @@ +import glob +import os.path +import platform + +def find_datafiles(): + system = platform.system() + if system == 'Windows': + file_ext = '*.exe' + else: + file_ext = '*.sh' + path = os.path.abspath(os.path.join(__path__[0], 'bootstrappers', file_ext)) + return [('', glob.glob(path))] diff --git a/src/update/update.py b/src/update/update.py new file mode 100644 index 0000000..b756cc9 --- /dev/null +++ b/src/update/update.py @@ -0,0 +1,118 @@ +from logging import getLogger +logger = getLogger('update') + +import contextlib +import io +import os +import platform +import requests +import tempfile +import widgetUtils +import webbrowser +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..86ee7e7 --- /dev/null +++ b/src/update/updater.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import application +import update +import platform +import logging +import output +from requests.exceptions import ConnectionError +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.") + output.speak("An exception occurred while attempting to update " + application.name + ". If this message persists, contact the " + application.name + " developers. More information about the exception has been written to the error log.",True) \ No newline at end of file diff --git a/src/update/utils.py b/src/update/utils.py new file mode 100644 index 0000000..9a20ab2 --- /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..44d06b6 --- /dev/null +++ b/src/update/wxUpdater.py @@ -0,0 +1,30 @@ +# -*- 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/wxUI/mainWindow.py b/src/wxUI/mainWindow.py index a579175..af7f56a 100644 --- a/src/wxUI/mainWindow.py +++ b/src/wxUI/mainWindow.py @@ -14,7 +14,7 @@ class mainWindow(wx.Frame): self.about = help_.Append(wx.NewId(), _(u"About {0}").format(application.name,)) self.about.Enable(False) self.check_for_updates = help_.Append(wx.NewId(), _(u"Check for updates")) - self.check_for_updates.Enable(False) + mb.Append(help_, _(u"Help")) self.SetMenuBar(mb)