Added the autoupdater module. Needs tests
This commit is contained in:
		| @@ -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" | ||||
| @@ -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() | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/update/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/update/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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))] | ||||
							
								
								
									
										118
									
								
								src/update/update.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/update/update.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
							
								
								
									
										16
									
								
								src/update/updater.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/update/updater.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										42
									
								
								src/update/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/update/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										30
									
								
								src/update/wxUpdater.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/update/wxUpdater.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user