Updater has been refactored

This commit is contained in:
Manuel Cortez 2015-02-16 17:21:27 -06:00
parent 10d0b8ba6b
commit d9d0efab2d
6 changed files with 207 additions and 255 deletions

View File

@ -1,2 +1,12 @@
import updater
import update_manager
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))]

114
src/updater/update.py Normal file
View File

@ -0,0 +1,114 @@
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:
return
available_version = available_update['current_version']
if not str(available_version) > str(current_version) or platform.system() not in available_update['downloads']:
return
available_description = available_update.get('description', None)
update_url = available_update ['downloads'][platform.system()]
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))

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
import os, sys, wx
from mysc import paths
import application, updater
from mysc.thread_utils import call_threaded
import logging as original_logger
log = original_logger.getLogger("update_manager")
def check_for_update(msg=False):
log.debug(u"Checking for updates...")
url = updater.find_update_url(application.update_url, application.version)
if url is None:
if msg == True:
wx.MessageDialog(None, _(u"Your TW Blue version is up to date"), _(u"Update"), style=wx.OK).ShowModal()
return
else:
log.debug(u"New version from %s " % url)
new_path = os.path.join(paths.app_path("delete-me"), 'updates', 'update.zip')
log.debug(u"Descargando actualización en %s" % new_path)
d = wx.MessageDialog(None, _(u"There's a new TW Blue version available. Would you like to download it now?"), _(u"New version for %s") % application.name, style=wx.YES|wx.NO|wx.ICON_WARNING)
if d.ShowModal() == wx.ID_YES:
progress = wx.ProgressDialog(_(u"Download in Progress"), _(u"Downloading the new version..."), parent=None, maximum=100, style = wx.PD_APP_MODAL)
def update(percent):
if percent == 100:
progress.Destroy()
else:
progress.Update(percent, _(u"Update"))
def update_complete():
wx.MessageDialog(None, _(u"The new TW Blue version has been downloaded and installed. Press OK to start the application."), _(u"Done!")).ShowModal()
sys.exit()
app_updater = updater.AutoUpdater(url, new_path, 'bootstrap.exe', app_path=paths.app_path(), postexecute=paths.app_path("TWBlue.exe"), finish_callback=update_complete, percentage_callback=update)
app_updater.start_update()
progress.ShowModal()
else:
return

View File

@ -1,220 +1,12 @@
#AutoUpdater
#Released under an MIT license
import logging
logger = logging.getLogger('updater')
# -*- coding: utf-8 -*-
import application
from urllib import FancyURLopener, URLopener
import urllib2
from functools import total_ordering
import hashlib
import os
try:
from czipfile import ZipFile
except ImportError:
from zipfile import ZipFile
import subprocess
import stat
import update
import platform
import shutil
import json
if platform.system() == 'Windows':
import win32api
#if platform.system() == "Windows":
from wxUpdater import *
class AutoUpdater(object):
def __init__(self, URL, save_location, bootstrapper, app_path, postexecute=None, password=None, MD5=None, percentage_callback=None, finish_callback=None):
"""Supply a URL/location/bootstrapper filename to download a zip file from
The finish_callback argument should be a Python function it'll call when done"""
#Let's download the file using urllib
self.complete = 0
self.finish_callback = finish_callback #What to do on exit
self.percentage_callback = percentage_callback or self.print_percentage_callback
self.URL = URL
self.bootstrapper = bootstrapper
#The application path on the Mac should be 1 directory up from where the .app file is.
tempstr = ""
if (platform.system() == "Darwin"):
for x in (app_path.split("/")):
if (".app" in x):
break
else:
tempstr = os.path.join(tempstr, x)
app_path = "/" + tempstr + "/"
#The post-execution path should include the .app file
tempstr = ""
for x in (postexecute.split("/")):
if (".app" in x):
tempstr = os.path.join(tempstr, x)
break
else:
tempstr = os.path.join(tempstr, x)
postexecute = "/" + tempstr
self.app_path = app_path
self.postexecute = postexecute
logging.info("apppath: " + str(app_path))
logging.info("postexecute: " + str(postexecute))
self.password = password
self.MD5 = MD5
self.save_location = save_location
#self.save_location contains the full path, including the blabla.zip
self.save_directory = os.path.join(*os.path.split(save_location)[:-1])
#self.save_directory doesn't contain the blabla.zip
def prepare_staging_directory(self):
if not os.path.exists(self.save_directory):
#We need to make all folders but the last one
os.makedirs(self.save_directory)
logger.info("Created staging directory %s" % self.save_directory)
def transfer_callback(self, count, bSize, tSize):
"""Callback to update percentage of download"""
percent = int(count*bSize*100/tSize)
self.percentage_callback(percent)
@staticmethod
def print_percentage_callback(percent):
print percent
def start_update(self):
"""Called to start the whole process"""
logger.debug("URL: %s SL: %s" % (self.URL, self.save_location))
self.prepare_staging_directory()
Listy = CustomURLOpener().retrieve(self.URL, self.save_location, reporthook=self.transfer_callback)
if self.MD5:
#Check the MD5
if self.MD5File(location) != self.MD5:
#ReDownload
self.start_update()
self.download_complete(Listy[0])
def MD5File(self, fileName):
"Custom function that will get the Md5 sum of our file"
file_reference=open(fileName, 'rb').read()
return hashlib.md5(file_reference).hexdigest()
def download_complete(self, location):
"""Called when the file is done downloading, and MD5 has been successfull"""
logger.debug("Download complete.")
zippy = ZipFile(location, mode='r')
extracted_path = os.path.join(self.save_directory, os.path.basename(location).strip(".zip"))
zippy.extractall(extracted_path, pwd=self.password)
bootstrapper_path = os.path.join(self.save_directory, self.bootstrapper) #where we will find our bootstrapper
old_bootstrapper_path = os.path.join(extracted_path, self.bootstrapper)
if os.path.exists(bootstrapper_path):
os.chmod(bootstrapper_path, 666)
os.remove(bootstrapper_path)
shutil.move(old_bootstrapper_path, self.save_directory) #move bootstrapper
os.chmod(bootstrapper_path, stat.S_IRUSR|stat.S_IXUSR)
if platform.system() == "Windows":
bootstrapper_command = r'%s' % bootstrapper_path
bootstrapper_args = r'"%s" "%s" "%s" "%s"' % (os.getpid(), extracted_path, self.app_path, self.postexecute)
win32api.ShellExecute(0, 'open', bootstrapper_command, bootstrapper_args, "", 5)
else:
#bootstrapper_command = [r'sh "%s" -l "%s" -d "%s" "%s"' % (bootstrapper_path, self.app_path, extracted_path, str(os.getpid()))]
bootstrapper_command = r'"%s" "%s" "%s" "%s" "%s"' % (bootstrapper_path, os.getpid(), extracted_path, self.app_path, self.postexecute)
shell = True
#logging.debug("Final bootstrapper command: %r" % bootstrapper_command)
subprocess.Popen([bootstrapper_command], shell=shell)
self.complete = 1
if callable(self.finish_callback):
self.finish_callback()
def cleanup(self):
"""Delete stuff"""
try:
shutil.rmtree(self.save_directory)
except any:
return
def find_update_url(URL, version):
"""Return a URL to an update of the application for the current platform at the given URL if one exists, or None""
Assumes Windows, Linux, or Mac"""
response = urllib2.urlopen(URL)
json_str = response.read().strip("\n")
json_p = json.loads(json_str)
if is_newer(version, json_p['current_version']):
if application.snapshot == False: return json_p['downloads'][platform.system()+platform.architecture()[0][:2]]
else: return json_p['downloads'][platform.system()]
def is_newer(local_version, remote_version):
"""Returns True if the remote version is newer than the local version."""
return Version(remote_version) > local_version
@total_ordering
class Version(object):
VERSION_QUALIFIERS = {
'alpha': 1,
'beta': 2,
'rc': 3
}
def __init__(self, version):
self.version = version
self.version_qualifier = None
self.version_qualifier_num = None
self.sub_version = None
if isinstance(version, basestring):
version = version.lower()
if '-' not in version:
for q in self.VERSION_QUALIFIERS:
if q in version:
self.version_qualifier = q
self.version_qualifier_num = self.VERSION_QUALIFIERS[q]
split_version = version.split(q)
self.version_number = float(split_version[0])
if len(split_version) > 1:
self.sub_version = split_version[1]
return
self.version_number= float(version)
return
split_version = version.split('-')
self.version_number= float(split_version[0])
self.version_qualifier = split_version [1]
self.version_qualifier_num = self.VERSION_QUALIFIERS[self.version_qualifier]
if len(split_version) == 3:
self.sub_version = int(split_version[2])
else:
self.version_number= float(version)
def __lt__(self, other):
if not isinstance(other, self.__class__):
other = Version(other)
if other.version_qualifier == self.version_qualifier == None:
return self.version_number< other.version_number
if self.version_number < other.version_number:
return True
elif self.version_number > other.version_number:
return False
if other.version_number == self.version_number and not other.version_qualifier_num and self.version_qualifier_num:
return True
if other.version_number == self.version_number and self.version_qualifier_num == self.version_qualifier_num and self.sub_version < other.sub_version:
return True
return self.version_qualifier_num < other.version_qualifier_num
def __gt__(self, other):
if not isinstance(other, self.__class__):
other = Version(other)
if other.version_qualifier == self.version_qualifier == None:
return self.version_number > other.version_number
if self.version_number < other.version_number:
return False
elif self.version_number > other.version_number:
return True
if other.version_number == self.version_number and not other.version_qualifier_num and self.version_qualifier_num:
return False
if other.version_number == self.version_number and self.version_qualifier_num == self.version_qualifier_num and self.sub_version > other.sub_version:
return True
return self.version_qualifier_num > other.version_qualifier_num
class CustomURLOpener(FancyURLopener):
def http_error_default(*a, **k):
return URLopener.http_error_default(*a, **k)
def do_update():
try:
update.perform_update(endpoint=application.update_url, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished)
except:
pass

42
src/updater/utils.py Normal file
View 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/updater/wxUpdater.py Normal file
View 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 new Lees version has been downloaded and installed. Press OK to start the application."), _(u"Done!")).ShowModal()