119 lines
4.7 KiB
Python
119 lines
4.7 KiB
Python
|
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))
|