update_manager: validate web type installations

Require that "web" installations provide release info to validate
existing installations.  For known web clients provide a fallback
that uses the manifest to validate the installation.

Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2023-06-16 17:55:44 -04:00
parent a7b9e5783d
commit a797dd0b50
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
1 changed files with 141 additions and 29 deletions

View File

@ -14,6 +14,8 @@ import zipfile
import time
import tempfile
import re
import json
from ...utils import source_info
from ...thirdparty.packagekit import enums as PkEnum
from . import base_config
from .base_deploy import BaseDeploy
@ -1174,6 +1176,7 @@ class PackageKitTransaction:
self._dl_remaining = bytes_remaining
self._notify_progress()
class WebClientDeploy(BaseDeploy):
def __init__(self,
config: ConfigHelper,
@ -1181,10 +1184,8 @@ class WebClientDeploy(BaseDeploy):
) -> None:
super().__init__(config, cmd_helper, prefix="Web Client")
self.repo = config.get('repo').strip().strip("/")
self.owner = self.repo.split("/", 1)[0]
self.owner, self.project_name = self.repo.split("/", 1)
self.path = pathlib.Path(config.get("path")).expanduser().resolve()
fm: FileManager = self.server.lookup_component("file_manager")
fm.add_reserved_path(f"update_manager {self.name}", self.path)
self.type = config.get('type')
def_channel = "stable"
if self.type == "web_beta":
@ -1202,6 +1203,8 @@ class WebClientDeploy(BaseDeploy):
f"Must be one of the following: stable, beta")
self.info_tags: List[str] = config.getlist("info_tags", [])
self.persistent_files: List[str] = []
self.warnings: List[str] = []
self.version: str = "?"
pfiles = config.getlist('persistent_files', None)
if pfiles is not None:
self.persistent_files = [pf.strip("/") for pf in pfiles]
@ -1209,38 +1212,150 @@ class WebClientDeploy(BaseDeploy):
raise config.error(
"Invalid value for option 'persistent_files': "
"'.version' can not be persistent")
self._valid: bool = True
self._is_prerelease: bool = False
self._is_fallback: bool = False
self._path_writable: bool = False
async def _validate_client_info(self) -> None:
self._valid = False
self._is_fallback = False
eventloop = self.server.get_event_loop()
self.warnings.clear()
if not self._path_writable:
self.warnings.append(
f"Location at option 'path: {self.path}' is not writable."
)
elif not self.path.is_dir():
self.warnings.append(
f"Location at option 'path: {self.path}' does not exist."
)
elif source_info.within_git_repo(self.path):
self.warnings.append(
f"Location at option 'path: {self.path}' is a git repo."
)
else:
rinfo = self.path.joinpath("release_info.json")
if rinfo.is_file():
try:
data = await eventloop.run_in_thread(rinfo.read_text)
uinfo: Dict[str, str] = json.loads(data)
project_name = uinfo["project_name"]
owner = uinfo["project_owner"]
self.version = uinfo["version"]
except Exception:
logging.exception("Failed to load release_info.json.")
else:
self._valid = True
detected_repo = f"{owner}/{project_name}"
if self.repo.lower() != detected_repo.lower():
self.warnings.append(
f"Value at option 'repo: {self.repo}' does not match "
f"detected repo '{detected_repo}', falling back to "
"detected version."
)
self.repo = detected_repo
self.owner = owner
self.project_name = project_name
else:
version_path = self.path.joinpath(".version")
if version_path.is_file():
version = await eventloop.run_in_thread(version_path.read_text)
self.version = version.strip()
self._valid = await self._detect_fallback()
if not self._valid:
self.warnings.append("Failed to validate client installation")
if self.server.is_debug_enabled():
self.log_info("Debug Enabled, overriding validity checks")
async def _detect_fallback(self) -> bool:
fallback_defs = {
"mainsail": "mainsail-crew",
"fluidd": "fluidd-core"
}
for fname in ("manifest.json", "manifest.webmanifest"):
manifest = self.path.joinpath(fname)
eventloop = self.server.get_event_loop()
if manifest.is_file():
try:
mtext = await eventloop.run_in_thread(manifest.read_text)
mdata: Dict[str, Any] = json.loads(mtext)
proj_name: str = mdata["name"].lower()
except Exception:
self.log_exc(f"Failed to load json from {manifest}")
continue
if proj_name in fallback_defs:
owner = fallback_defs[proj_name]
detected_repo = f"{owner}/{proj_name}"
if detected_repo != self.repo.lower():
self.warnings.append(
f"Value at option 'repo: {self.repo}' does not match "
f"detected repo '{detected_repo}', falling back to "
"detected version."
)
self.repo = detected_repo
self.owner = owner
self.project_name = proj_name
self._is_fallback = True
return True
return False
async def initialize(self) -> Dict[str, Any]:
fm: FileManager = self.server.lookup_component("file_manager")
self._path_writable = not fm.check_reserved_path(
self.path, need_write=True, raise_error=False
)
if self._path_writable:
fm.add_reserved_path(f"update_manager {self.name}", self.path)
await self._validate_client_info()
storage = await super().initialize()
self.version: str = storage.get('version', "?")
if self.version == "?":
self.version = storage.get("version", "?")
self.remote_version: str = storage.get('remote_version', "?")
self.last_error: str = storage.get('last_error', "")
dl_info: List[Any] = storage.get('dl_info', ["?", "?", 0])
self.dl_info: Tuple[str, str, int] = cast(
Tuple[str, str, int], tuple(dl_info))
logging.info(f"\nInitializing Client Updater: '{self.name}',"
f"\nChannel: {self.channel}"
f"\npath: {self.path}")
if not self.needs_refresh():
self._log_client_info()
return storage
async def _get_local_version(self) -> None:
version_path = self.path.joinpath(".version")
if version_path.is_file():
event_loop = self.server.get_event_loop()
version = await event_loop.run_in_thread(version_path.read_text)
self.version = version.strip()
else:
self.version = "?"
def _log_client_info(self) -> None:
warn_str = ""
if self.warnings:
warn_str = "\nWarnings:\n"
warn_str += "\n".join([f" {item}" for item in self.warnings])
dl_url, content_type, size = self.dl_info
logging.info(
f"Web Client {self.name} Detected:\n"
f"Repo: {self.repo}\n"
f"Channel: {self.channel}\n"
f"Path: {self.path}\n"
f"Local Version: {self.version}\n"
f"Remote Version: {self.remote_version}\n"
f"Valid: {self._valid}\n"
f"Fallback Client Detected: {self._is_fallback}\n"
f"Pre-release: {self._is_prerelease}\n"
f"Download Url: {dl_url}\n"
f"Download Size: {size}\n"
f"Content Type: {content_type}"
f"{warn_str}"
)
async def refresh(self) -> None:
try:
await self._get_local_version()
if not self._valid:
await self._validate_client_info()
await self._get_remote_version()
except Exception:
logging.exception("Error Refreshing Client")
self._log_client_info()
self._save_state()
async def _get_remote_version(self) -> None:
if not self._valid:
self.log_info("Invalid Web Installation, aborting remote refresh")
return
# Remote state
if self.channel == "stable":
resource = f"repos/{self.repo}/releases/latest"
@ -1279,14 +1394,7 @@ class WebClientDeploy(BaseDeploy):
content_type: str = release_asset.get('content_type', "?")
size: int = release_asset.get('size', 0)
self.dl_info = (dl_url, content_type, size)
logging.info(
f"Github client Info Received:\nRepo: {self.name}\n"
f"Local Version: {self.version}\n"
f"Remote Version: {self.remote_version}\n"
f"Pre-release: {result.get('prerelease', '?')}\n"
f"url: {dl_url}\n"
f"size: {size}\n"
f"Content Type: {content_type}")
self._is_prerelease = result.get('prerelease', False)
def get_persistent_data(self) -> Dict[str, Any]:
storage = super().get_persistent_data()
@ -1297,6 +1405,10 @@ class WebClientDeploy(BaseDeploy):
return storage
async def update(self) -> bool:
if not self._valid:
raise self.server.error(
f"Web Client {self.name}: Invalid install detected, aborting update"
)
if self.remote_version == "?":
await self._get_remote_version()
if self.remote_version == "?":
@ -1331,12 +1443,10 @@ class WebClientDeploy(BaseDeploy):
finally:
await event_loop.run_in_thread(td.cleanup)
self.version = self.remote_version
version_path = self.path.joinpath(".version")
if not version_path.exists():
await event_loop.run_in_thread(
version_path.write_text, self.version)
await self._validate_client_info()
self.cmd_helper.notify_update_response(
f"Client Update Finished: {self.name}", is_complete=True)
self._log_client_info()
self._save_state()
return True
@ -1374,7 +1484,9 @@ class WebClientDeploy(BaseDeploy):
'configured_type': self.type,
'channel': self.channel,
'info_tags': self.info_tags,
'last_error': self.last_error
'last_error': self.last_error,
'is_valid': self._valid,
'warnings': self.warnings
}
def load_component(config: ConfigHelper) -> UpdateManager: