Added core module to package
This commit is contained in:
parent
0066d98de9
commit
7967168df8
12
updater/__init__.py
Normal file
12
updater/__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))]
|
179
updater/core.py
Normal file
179
updater/core.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
""" Base class implementation for core features present in the updater module.
|
||||||
|
|
||||||
|
This is the updater core class which provides all facilities other implementation should rely in.
|
||||||
|
This class should not be used directly, use a derived class instead.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
import logging
|
||||||
|
from pubsub import pub # type: ignore
|
||||||
|
from platform_utils import paths # type: ignore
|
||||||
|
from typing import Optional, Dict, Tuple, Union, Any
|
||||||
|
|
||||||
|
log = logging.getLogger("updater.core")
|
||||||
|
|
||||||
|
class updaterCore(object):
|
||||||
|
""" Base class for all updater implementations.
|
||||||
|
|
||||||
|
Implementations must add user interaction methods and call logic for all methods present in this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, endpoint: str, current_version: str, app_name: str = "", password: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
:param endpoint: The URl endpoint where the module should retrieve update information. It must return a json valid response or a non 200 HTTP status code.
|
||||||
|
:type endpoint: str
|
||||||
|
:param current_version: Application's current version.
|
||||||
|
:type current_version: str
|
||||||
|
:param app_name: Name of the application.
|
||||||
|
(default is empty)
|
||||||
|
:type app_name: str
|
||||||
|
:param password: Password for update zipfile.
|
||||||
|
:type password: bytes
|
||||||
|
"""
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.current_version = current_version
|
||||||
|
self.app_name = app_name
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def create_session(self) -> None:
|
||||||
|
""" Creates a requests session for calling update server. The session will add an user agent based in parameters passed to :py:class:`updater.core.updaterCore`'s constructor. """
|
||||||
|
user_agent: str = "%s/%r" % (self.app_name, self.current_version)
|
||||||
|
self.session: requests.Session = requests.Session()
|
||||||
|
self.session.headers['User-Agent'] = self.session.headers['User-Agent'] + user_agent
|
||||||
|
|
||||||
|
def get_update_information(self) -> Dict[str, Any]:
|
||||||
|
""" Calls the provided URL endpoint and returns information about the available update sent by the server. The format should adhere to the json specifications for updates.
|
||||||
|
|
||||||
|
If the server returns a status code different to 200 or the json file is not valid, this will raise either a :external:py:exc:`requests.RequestException` or a :external:py:exc:`json.JSONDecodeError`.
|
||||||
|
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
response: requests.Response = self.session.get(self.endpoint)
|
||||||
|
response.raise_for_status()
|
||||||
|
content = response.json()
|
||||||
|
return content
|
||||||
|
|
||||||
|
def get_version_data(self, content: Dict[str, Any]) -> Tuple[Union[bool, str], Union[bool, str], Union[bool, str]]:
|
||||||
|
""" Parses the dictionary returned by :py:func:`updater.core.updaterCore.get_update_information` and, if there is a new update available, returns information about it in a tuple.
|
||||||
|
|
||||||
|
the module checks whether :py:attr:`updater.core.updaterCore.current_version` is different to the version reported in the update file, and the json specification file contains a binary link for the user's architecture. If both of these conditions are True, a tuple is returned with (new_version, update_description, update_url).
|
||||||
|
|
||||||
|
If there is no update available, or binaries for the user architecture, then a tuple with Falsy values is returned.
|
||||||
|
|
||||||
|
:returns: tuple with update information or False values.
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
|
available_version = content["current_version"]
|
||||||
|
if available_version == self.current_version:
|
||||||
|
return (False, False, False)
|
||||||
|
available_description = content["description"]
|
||||||
|
update_url = content ['downloads'][platform.system()+platform.architecture()[0][:2]]
|
||||||
|
return (available_version, available_description, update_url)
|
||||||
|
|
||||||
|
def download_update(self, update_url: str, update_destination: str, chunk_size: int = io.DEFAULT_BUFFER_SIZE) -> str:
|
||||||
|
""" Downloads an update URL and notifies all subscribers of the download progress.
|
||||||
|
|
||||||
|
This function will send a pubsub notification every time the download progress updates by using :py:func:`pubsub.pub.sendMessage` under the topic "updater.update-progress".
|
||||||
|
You might subscribe to this notification by using :py:func:`pubsub.pub.subscribe` with a function with this signature:
|
||||||
|
|
||||||
|
``def receive_progress(total_downloaded: int, total_size: int):``
|
||||||
|
|
||||||
|
In this function, it is possible to update the UI progress bar if needed.
|
||||||
|
Don't forget to call :py:func:`pubsub.pub.unsubscribe` at the end of the update.
|
||||||
|
|
||||||
|
:param update_url: Direct link to update zip file.
|
||||||
|
:type update_url: str
|
||||||
|
:param update_destination: Destination path to save the update file
|
||||||
|
:type update_destination: str
|
||||||
|
:param chunk_size: chunk size for downloading the update (default to :py:data:`io.DEFAULT_BUFFER_SIZE`)
|
||||||
|
:type chunk_size: int
|
||||||
|
:returns: The update file path in the system.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
total_downloaded: int = 0
|
||||||
|
total_size: int = 0
|
||||||
|
with io.open(update_destination, 'w+b') as outfile:
|
||||||
|
download: requests.Response = self.session.get(update_url, stream=True)
|
||||||
|
total_size = int(download.headers.get('content-length', 0))
|
||||||
|
log.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)
|
||||||
|
pub.sendMessage("updater.update-progress", total_downloaded, total_size)
|
||||||
|
log.debug("Update downloaded")
|
||||||
|
return update_destination
|
||||||
|
|
||||||
|
def extract_update(self, update_archive: str, destination: str) -> str:
|
||||||
|
""" Given an update archive, extracts it. Returns the directory to which it has been extracted.
|
||||||
|
|
||||||
|
:param update_archive: Path to the update file.
|
||||||
|
:type update_archive: str
|
||||||
|
:param destination: Path to extract the archive. User must have permission to do file operations on the path.
|
||||||
|
:type destination: str
|
||||||
|
:returns: Path where the archive has been extracted.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
with contextlib.closing(zipfile.ZipFile(update_archive)) as archive:
|
||||||
|
if self.password:
|
||||||
|
archive.setpassword(self.password)
|
||||||
|
archive.extractall(path=destination)
|
||||||
|
log.debug("Update extracted")
|
||||||
|
return destination
|
||||||
|
|
||||||
|
def move_bootstrap(self, extracted_path: str) -> str:
|
||||||
|
""" Moves the bootstrapper binary from the update extraction folder to a working path, so it will be able to perform operations under the update directory later.
|
||||||
|
|
||||||
|
:param extracted_path: Path to which the update file has been extracted.
|
||||||
|
:type extracted_path: str
|
||||||
|
:returns: The path to the bootstrap binary to be run to actually perform the update.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
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, self.bootstrap_name())
|
||||||
|
new_bootstrap_path = os.path.join(working_path, self.bootstrap_name())
|
||||||
|
os.rename(downloaded_bootstrap, new_bootstrap_path)
|
||||||
|
return new_bootstrap_path
|
||||||
|
|
||||||
|
def execute_bootstrap(self, bootstrap_path: str, source_path: str) -> None:
|
||||||
|
""" Executes the bootstrapper binary, which will move the files from the update directory to the app folder, finishing with the update process.
|
||||||
|
|
||||||
|
:param bootstrap_path: Path to the bootstrap binary that will perform the update, as returned by :py:func:`move_bootstrap`
|
||||||
|
:type bootstrap_path: str
|
||||||
|
:param source_path: Path where the update file was extracted, as returned by :py:func:`extract_update`
|
||||||
|
:type source_path: str
|
||||||
|
"""
|
||||||
|
arguments = r'"%s" "%s" "%s" "%s"' % (os.getpid(), source_path, paths.app_path(), paths.get_executable())
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
import win32api # type: ignore
|
||||||
|
win32api.ShellExecute(0, 'open', bootstrap_path, arguments, '', 5)
|
||||||
|
else:
|
||||||
|
import subprocess
|
||||||
|
self.make_executable(bootstrap_path)
|
||||||
|
subprocess.Popen(['%s %s' % (bootstrap_path, arguments)], shell=True)
|
||||||
|
log.info("Bootstrap executed")
|
||||||
|
|
||||||
|
def bootstrap_name(self) -> str:
|
||||||
|
""" Returns the name of the bootstrapper, based in user platform.
|
||||||
|
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
return 'bootstrap.exe'
|
||||||
|
elif platform.system() == 'Darwin':
|
||||||
|
return 'bootstrap-mac.sh'
|
||||||
|
return 'bootstrap-lin.sh'
|
||||||
|
|
||||||
|
def make_executable(self, path: str) -> None:
|
||||||
|
""" Set execution permissions in a script on Unix platforms. """
|
||||||
|
import stat
|
||||||
|
st = os.stat(path)
|
||||||
|
os.chmod(path, st.st_mode | stat.S_IEXEC)
|
Loading…
Reference in New Issue
Block a user