mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2024-11-22 19:28:09 -06:00
Updater has been refactored
This commit is contained in:
parent
10d0b8ba6b
commit
d9d0efab2d
@ -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
114
src/updater/update.py
Normal 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))
|
@ -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
|
||||
|
@ -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"""
|
||||
def do_update():
|
||||
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)
|
||||
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
42
src/updater/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/updater/wxUpdater.py
Normal file
30
src/updater/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 new Lees version has been downloaded and installed. Press OK to start the application."), _(u"Done!")).ShowModal()
|
Loading…
Reference in New Issue
Block a user