async def async_save_file(location, content): """Save files.""" logger = Logger("hacs.download.save") logger.debug(f"Saving {location}") mode = "w" encoding = "utf-8" errors = "ignore" if not isinstance(content, str): mode = "wb" encoding = None errors = None try: async with aiofiles.open(location, mode=mode, encoding=encoding, errors=errors) as outfile: await outfile.write(content) outfile.close() # Create gz for .js files if os.path.isfile(location): if location.endswith(".js") or location.endswith(".css"): with open(location, "rb") as f_in: with gzip.open(location + ".gz", "wb") as f_out: shutil.copyfileobj(f_in, f_out) except Exception as error: # pylint: disable=broad-except msg = "Could not write data to {} - {}".format(location, error) logger.debug(msg) return os.path.exists(location)
async def async_download_file(url): """ Download files, and return the content. """ hacs = get_hacs() logger = Logger("hacs.download.downloader") if url is None: return # There is a bug somewhere... TODO: Find that bug.... if "tags/" in url: url = url.replace("tags/", "") logger.debug(f"Downloading {url}") result = None with async_timeout.timeout(60, loop=hacs.hass.loop): request = await hacs.session.get(url) # Make sure that we got a valid result if request.status == 200: result = await request.read() else: raise HacsException( "Got status code {} when trying to download {}".format( request.status, url)) return result
def print(self): """Print the current configuration to the log.""" logger = Logger("hacs.configuration") config = self.to_json() for key in config: if key in ["config", "config_entry", "options", "token"]: continue logger.debug(f"{key}: {config[key]}")
async def async_save_file(location, content): """Save files.""" logger = Logger("hacs.download.save") logger.debug(f"Saving {location}") mode = "w" encoding = "utf-8" errors = "ignore" if not isinstance(content, str): mode = "wb" encoding = None errors = None try: async with aiofiles.open(location, mode=mode, encoding=encoding, errors=errors) as outfile: await outfile.write(content) outfile.close() # Create gz for .js files if os.path.isfile(location): if location.endswith(".js") or location.endswith(".css"): with open(location, "rb") as f_in: with gzip.open(location + ".gz", "wb") as f_out: shutil.copyfileobj(f_in, f_out) # Remove with 2.0 if "themes" in location and location.endswith(".yaml"): filename = location.split("/")[-1] base = location.split("/themes/")[0] combined = f"{base}/themes/{filename}" if os.path.exists(combined): logger.info(f"Removing old theme file {combined}") os.remove(combined) except Exception as error: # pylint: disable=broad-except msg = "Could not write data to {} - {}".format(location, error) logger.error(msg) return False return os.path.exists(location)
class BackupNetDaemon: """BackupNetDaemon.""" def __init__(self, repository): """Initialize.""" self.repository = repository self.logger = Logger("hacs.backup") self.backup_path = (tempfile.gettempdir() + "/hacs_persistent_netdaemon/" + repository.data.name) def create(self): """Create a backup in /tmp""" if os.path.exists(self.backup_path): shutil.rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) os.makedirs(self.backup_path, exist_ok=True) for filename in os.listdir(self.repository.content.path.local): if filename.endswith(".yaml"): source_file_name = f"{self.repository.content.path.local}/{filename}" target_file_name = f"{self.backup_path}/{filename}" shutil.copyfile(source_file_name, target_file_name) def restore(self): """Create a backup in /tmp""" if os.path.exists(self.backup_path): for filename in os.listdir(self.backup_path): if filename.endswith(".yaml"): source_file_name = f"{self.backup_path}/{filename}" target_file_name = ( f"{self.repository.content.path.local}/{filename}") shutil.copyfile(source_file_name, target_file_name) def cleanup(self): """Create a backup in /tmp""" if os.path.exists(self.backup_path): shutil.rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) self.logger.debug(f"Backup dir {self.backup_path} cleared")
class Backup: """Backup.""" def __init__(self, local_path, backup_path=BACKUP_PATH): """initialize.""" self.logger = Logger("hacs.backup") self.local_path = local_path self.backup_path = backup_path self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}" def create(self): """Create a backup in /tmp""" if not os.path.exists(self.local_path): return if os.path.exists(self.backup_path): shutil.rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) os.makedirs(self.backup_path, exist_ok=True) try: if os.path.isfile(self.local_path): shutil.copyfile(self.local_path, self.backup_path_full) os.remove(self.local_path) else: shutil.copytree(self.local_path, self.backup_path_full) shutil.rmtree(self.local_path) while os.path.exists(self.local_path): sleep(0.1) self.logger.debug( f"Backup for {self.local_path}, created in {self.backup_path_full}" ) except Exception: # pylint: disable=broad-except pass def restore(self): """Restore from backup.""" if not os.path.exists(self.backup_path_full): return if os.path.isfile(self.backup_path_full): if os.path.exists(self.local_path): os.remove(self.local_path) shutil.copyfile(self.backup_path_full, self.local_path) else: if os.path.exists(self.local_path): shutil.rmtree(self.local_path) while os.path.exists(self.local_path): sleep(0.1) shutil.copytree(self.backup_path_full, self.local_path) self.logger.debug( f"Restored {self.local_path}, from backup {self.backup_path_full}") def cleanup(self): """Cleanup backup files.""" if os.path.exists(self.backup_path): shutil.rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) self.logger.debug(f"Backup dir {self.backup_path} cleared")
class Backup: """Backup.""" def __init__(self, local_path): """initialize.""" self.logger = Logger("hacs.backup") self.local_path = local_path self.backup_path = "/tmp/hacs_backup" def create(self): """Create a backup in /tmp""" if os.path.exists(self.backup_path): rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) os.makedirs(self.backup_path, exist_ok=True) try: if os.path.isfile(self.local_path): copy2(self.local_path, self.backup_path) os.remove(self.local_path) else: copy_tree(self.local_path, self.backup_path) rmtree(self.local_path) while os.path.exists(self.local_path): sleep(0.1) self.logger.debug( f"Backup for {self.local_path}, created in {self.backup_path}") except Exception: # pylint: disable=broad-except pass def restore(self): """Restore from backup.""" if os.path.isfile(self.local_path): os.remove(self.local_path) else: rmtree(self.local_path) while os.path.exists(self.local_path): sleep(0.1) copy2(self.backup_path, self.local_path) self.logger.debug( f"Restored {self.local_path}, from backup {self.backup_path}") def cleanup(self): """Cleanup backup files.""" rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) self.logger.debug(f"Backup dir {self.backup_path} cleared")
class HacsAPI(HacsWebResponse): """HacsAPI class.""" name = "hacsapi" def __init__(self): """Initialize.""" self.logger = Logger("hacs.api") self.url = self.hacsapi + "/{endpoint}" async def post(self, request, endpoint): # pylint: disable=unused-argument """Handle HACS API requests.""" if self.system.disabled: return web.Response(status=404) self.endpoint = endpoint self.postdata = await request.post() self.raw_headers = request.raw_headers self.request = request self.logger.debug(f"Endpoint ({endpoint}) called") if self.configuration.dev: self.logger.debug(f"Raw headers ({self.raw_headers})") self.logger.debug(f"Postdata ({self.postdata})") if self.endpoint in APIRESPONSE: try: response = APIRESPONSE[self.endpoint] response = await response.response(self) except Exception as exception: render = self.render(f"error", message=exception) return web.Response( body=render, content_type="text/html", charset="utf-8" ) else: # Return default response. response = await APIRESPONSE["generic"].response(self) # set headers response.headers["Cache-Control"] = "max-age=0, must-revalidate" # serve the response return response
class HacsData(Hacs): """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") def check_corrupted_files(self): """Return True if one (or more) of the files are corrupted.""" for store in STORES: path = f"{self.system.config_path}/.storage/{STORES[store]}" if os.path.exists(path): if os.stat(path).st_size == 0: # File is empty (corrupted) return True return False def read(self, store): """Return data from a store.""" path = f"{self.system.config_path}/.storage/{STORES[store]}" content = None if os.path.exists(path): with open(path, "r", encoding="utf-8") as storefile: content = storefile.read() content = json.loads(content) return content async def async_write(self): """Write content to the store files.""" if self.system.status.background_task: return self.logger.debug("Saving data") # Hacs await async_save_to_store(self.hass, "hacs", {"view": self.configuration.frontend_mode}) # Repositories content = {} for repository in self.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.information.authors, "topics": repository.information.topics, "category": repository.information.category, "description": repository.information.description, "full_name": repository.information.full_name, "hide": repository.status.hide, "installed_commit": repository.versions.installed_commit, "installed": repository.status.installed, "last_commit": repository.versions.available_commit, "last_release_tag": repository.versions.available, "repository_manifest": repository_manifest, "name": repository.information.name, "new": repository.status.new, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "version_installed": repository.versions.installed, } await async_save_to_store(self.hass, "repositories", content) self.hass.bus.async_fire("hacs/repository", {}) self.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = {} repositories = {} try: hacs = await async_load_from_store(self.hass, "hacs") except KeyError: await async_save_to_store(self.hass, "hacs", self.data.read("hacs")["data"]) hacs = await async_load_from_store(self.hass, "hacs") try: repositories = await async_load_from_store(self.hass, "repositories") except KeyError: await async_save_to_store(self.hass, "repositories", self.data.read("repositories")["data"]) repositories = await async_load_from_store(self.hass, "repositories") try: if self.check_corrupted_files(): # Coruptted installation self.logger.critical( "Restore failed one or more files are corrupted!") return False if hacs is None and repositories is None: # Assume new install return True self.logger.info("Restore started") # Hacs self.configuration.frontend_mode = hacs.get("view", "Grid") # Repositories repositories = repositories for entry in repositories: repo = repositories[entry] if repo["full_name"] == "custom-components/hacs": # Skip the old repo location continue if not self.is_known(repo["full_name"]): await self.register_repository(repo["full_name"], repo["category"], False) repository = self.get_by_name(repo["full_name"]) if repository is None: self.logger.error(f"Did not find {repo['full_name']}") continue # Restore repository attributes if repo.get("authors") is not None: repository.information.authors = repo["authors"] if repo.get("topics", []): repository.information.topics = repo["topics"] if repo.get("description") is not None: repository.information.description = repo["description"] if repo.get("name") is not None: repository.information.name = repo["name"] if repo.get("hide") is not None: repository.status.hide = repo["hide"] if repo.get("installed") is not None: repository.status.installed = repo["installed"] if repository.status.installed: repository.status.first_install = False if repo.get("selected_tag") is not None: repository.status.selected_tag = repo["selected_tag"] if repo.get("repository_manifest") is not None: repository.repository_manifest = HacsManifest.from_dict( repo["repository_manifest"]) if repo.get("show_beta") is not None: repository.status.show_beta = repo["show_beta"] if repo.get("last_commit") is not None: repository.versions.available_commit = repo["last_commit"] repository.information.uid = entry if repo.get("last_release_tag") is not None: repository.releases.last_release = repo["last_release_tag"] repository.versions.available = repo["last_release_tag"] if repo.get("new") is not None: repository.status.new = repo["new"] if repo["full_name"] == "hacs/integration": repository.versions.installed = VERSION repository.status.installed = True if "b" in VERSION: repository.status.show_beta = True elif repo.get("version_installed") is not None: repository.versions.installed = repo["version_installed"] if repo.get("installed_commit") is not None: repository.versions.installed_commit = repo[ "installed_commit"] self.logger.info("Restore done") except Exception as exception: self.logger.critical( f"[{exception}] Restore Failed! see https://github.com/hacs/integration/issues/639 for more details." ) return False return True
class WebClient: """Web client.""" def __init__(self, session=None, logger=None): """ Initialize. Sample Usage: from integrationhelper.webclient import WebClient url = "https://sample.com/api" webclient = WebClient() print(await webclient.async_get_json(url)) """ self.session = session if logger is not None: self.logger = logger else: from integrationhelper import Logger self.logger = Logger(__name__) @backoff.on_exception(backoff.expo, Exception, max_tries=3) async def async_get_json(self, url: str, custom_headers: dict = None): """Get json response from server.""" try: assert isinstance(url, str) except AssertionError: self.logger.error(f"({url}) is not a string.") return None try: assert custom_headers is None or isinstance(custom_headers, dict) except AssertionError: self.logger.error(f"({custom_headers}) is not a dict.") return None headers = {"Content-Type": "application/json"} if custom_headers is not None: for header in custom_headers: headers[header] = custom_headers[header] jsondata = None try: if self.session is not None: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await self.session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return jsondata jsondata = await response.json() else: async with aiohttp.ClientSession() as session: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return jsondata jsondata = await response.json() self.logger.debug(jsondata) except asyncio.TimeoutError as error: self.logger.error( f"Timeout error fetching information from {url} - ({error})") except (KeyError, TypeError) as error: self.logger.error( f"Error parsing information from {url} - ({error})") except (aiohttp.ClientError, socket.gaierror) as error: self.logger.error( f"Error fetching information from {url} - ({error})") except Exception as error: # pylint: disable=broad-except self.logger.error(f"Something really wrong happend! - ({error})") return jsondata @backoff.on_exception(backoff.expo, Exception, max_tries=3) async def async_get_text(self, url: str, custom_headers: dict = None): """Get text response from server.""" try: assert isinstance(url, str) except AssertionError: self.logger.error(f"({url}) is not a string.") return None try: assert url is None or isinstance(custom_headers, dict) except AssertionError: self.logger.error(f"({custom_headers}) is not a dict.") return None headers = {"Content-Type": "application/json"} if custom_headers is not None: for header in custom_headers: headers[header] = custom_headers[header] textdata = None try: if self.session is not None: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await self.session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return textdata textdata = await response.text() else: async with aiohttp.ClientSession() as session: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return textdata textdata = await response.text() self.logger.debug(textdata) except asyncio.TimeoutError as error: self.logger.error( f"Timeout error fetching information from {url} - ({error})") except (KeyError, TypeError) as error: self.logger.error( f"Error parsing information from {url} - ({error})") except (aiohttp.ClientError, socket.gaierror) as error: self.logger.error( f"Error fetching information from {url} - ({error})") except Exception as error: # pylint: disable=broad-except self.logger.error(f"Something really wrong happend! - ({error})") return textdata
class HacsRepository(Hacs): """HacsRepository.""" def __init__(self): """Set up HacsRepository.""" self.content = RepositoryContent() self.content.path = RepositoryPath() self.information = RepositoryInformation() self.repository_object = None self.status = RepositoryStatus() self.repository_manifest = None self.validate = Validate() self.releases = RepositoryReleases() self.versions = RepositoryVersions() self.pending_restart = False self.logger = None @property def pending_upgrade(self): """Return pending upgrade.""" if self.status.installed: if self.display_installed_version != self.display_available_version: return True return False @property def ref(self): """Return the ref.""" if self.status.selected_tag is not None: if self.status.selected_tag == self.information.default_branch: return self.information.default_branch return "tags/{}".format(self.status.selected_tag) if self.releases.releases: return "tags/{}".format(self.versions.available) return self.information.default_branch @property def custom(self): """Return flag if the repository is custom.""" if self.information.full_name.split("/")[0] in [ "custom-components", "custom-cards", ]: return False if self.information.full_name in self.common.default: return False return True @property def can_install(self): """Return bool if repository can be installed.""" target = None if self.information.homeassistant_version is not None: target = self.information.homeassistant_version if self.repository_manifest is not None: if self.repository_manifest.homeassistant is not None: target = self.repository_manifest.homeassistant if target is not None: if self.releases.releases: if LooseVersion(self.system.ha_version) < LooseVersion(target): return False return True @property def display_name(self): """Return display name.""" name = None if self.information.category == "integration": if self.manifest is not None: name = self.manifest["name"] if self.repository_manifest is not None: name = self.repository_manifest.name if name is not None: return name if self.information.name: name = self.information.name.replace("-", " ").replace("_", " ").title() if name is not None: return name name = self.information.full_name return name @property def display_status(self): """Return display_status.""" if self.status.new: status = "new" elif self.pending_restart: status = "pending-restart" elif self.pending_upgrade: status = "pending-upgrade" elif self.status.installed: status = "installed" else: status = "default" return status @property def display_status_description(self): """Return display_status_description.""" description = { "default": "Not installed.", "pending-restart": "Restart pending.", "pending-upgrade": "Upgrade pending.", "installed": "No action required.", "new": "This is a newly added repository.", } return description[self.display_status] @property def display_installed_version(self): """Return display_authors""" if self.versions.installed is not None: installed = self.versions.installed else: if self.versions.installed_commit is not None: installed = self.versions.installed_commit else: installed = "" return installed @property def display_available_version(self): """Return display_authors""" if self.versions.available is not None: available = self.versions.available else: if self.versions.available_commit is not None: available = self.versions.available_commit else: available = "" return available @property def display_version_or_commit(self): """Does the repositoriy use releases or commits?""" if self.versions.installed is not None: version_or_commit = "version" else: version_or_commit = "commit" return version_or_commit @property def main_action(self): """Return the main action.""" actions = { "new": "INSTALL", "default": "INSTALL", "installed": "REINSTALL", "pending-restart": "REINSTALL", "pending-upgrade": "UPGRADE", } return actions[self.display_status] async def common_validate(self): """Common validation steps of the repository.""" # Attach helpers self.validate.errors = [] self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Step 1: Make sure the repository exist. self.logger.debug("Checking repository.") try: self.repository_object = await self.github.get_repo( self.information.full_name) except Exception as exception: # Gotta Catch 'Em All if not self.system.status.startup: self.logger.error(exception) self.validate.errors.append("Repository does not exist.") return # Step 2: Make sure the repository is not archived. if self.repository_object.archived: self.validate.errors.append("Repository is archived.") return # Step 3: Make sure the repository is not in the blacklist. if self.information.full_name in self.common.blacklist: self.validate.errors.append("Repository is in the blacklist.") return # Step 4: default branch self.information.default_branch = self.repository_object.default_branch # Step 5: Get releases. await self.get_releases() # Set repository name self.information.name = self.information.full_name.split("/")[1] async def common_registration(self): """Common registration steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository if self.repository_object is None: self.repository_object = await self.github.get_repo( self.information.full_name) # Set id self.information.uid = str(self.repository_object.id) # Set topics self.information.topics = self.repository_object.topics # Set description if self.repository_object.description: self.information.description = self.repository_object.description async def common_update(self): """Common information update steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update description if self.repository_object.description: self.information.description = self.repository_object.description # Update default branch if self.information.full_name != "custom-components/hacs": self.information.default_branch = self.repository_object.default_branch else: self.information.default_branch = "next" # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Update last updaeted self.information.last_updated = self.repository_object.pushed_at # Update topics self.information.topics = self.repository_object.topics # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" await self.get_info_md_content() # Update releases await self.get_releases() async def install(self): """Common installation steps of the repository.""" self.validate.errors = [] await self.update_repository() if self.status.installed and not self.content.single: backup = Backup(self.content.path.local) backup.create() validate = await self.download_content(self.validate, self.content.path.remote, self.content.path.local, self.ref) if validate.errors: for error in validate.errors: self.logger.error(error) if self.status.installed and not self.content.single: backup.restore() if self.status.installed and not self.content.single: backup.cleanup() if validate.success: if self.information.full_name not in self.common.installed: if self.information.full_name != "custom-components/hacs": self.common.installed.append(self.information.full_name) self.status.installed = True self.versions.installed_commit = self.versions.available_commit if self.status.selected_tag is not None: self.versions.installed = self.status.selected_tag else: self.versions.installed = self.versions.available if self.information.category == "integration": if (self.config_flow and self.information.full_name != "custom-components/hacs"): await self.reload_custom_components() else: self.pending_restart = True async def download_content(self, validate, directory_path, local_directory, ref): """Download the content of a directory.""" try: # Get content if self.content.single: contents = self.content.objects else: contents = await self.repository_object.get_contents( directory_path, self.ref) for content in contents: if content.type == "dir" and self.content.path.remote != "": await self.download_content(validate, content.path, local_directory, ref) continue if self.information.category == "plugin": if not content.name.endswith(".js"): if self.content.path.remote != "dist": continue self.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue # Save the content of the file. if self.content.single: local_directory = self.content.path.local else: _content_path = content.path _content_path = _content_path.replace( f"{self.content.path.remote}/", "") local_directory = f"{self.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = f"{local_directory}/{content.name}" result = await async_save_file(local_file_path, filecontent) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except SystemError: pass return validate async def get_repository_manifest_content(self): """Get the content of the hacs.json file.""" try: manifest = await self.repository_object.get_contents( "hacs.json", self.ref) self.repository_manifest = HacsManifest( json.loads(manifest.content)) except AIOGitHubException: # Gotta Catch 'Em All pass async def get_info_md_content(self): """Get the content of info.md""" from ..handler.template import render_template info = None info_files = ["info", "info.md"] if self.repository_manifest is not None: if self.repository_manifest.render_readme: info_files = ["readme", "readme.md"] try: root = await self.repository_object.get_contents("", self.ref) for file in root: if file.name.lower() in info_files: info = await self.repository_object.get_contents( file.name, self.ref) break if info is None: self.information.additional_info = "" else: info = await self.github.render_markdown(info.content) info = info.replace("<", "<") info = info.replace("<h3>", "<h6>").replace("</h3>", "</h6>") info = info.replace("<h2>", "<h5>").replace("</h2>", "</h5>") info = info.replace("<h1>", "<h4>").replace("</h1>", "</h4>") info = info.replace("<code>", "<code class='codeinfo'>") info = info.replace( '<a href="http', '<a rel="noreferrer" target="_blank" href="http') info = info.replace("<ul>", "") info = info.replace("</ul>", "") info += "</br>" self.information.additional_info = render_template(info, self) except Exception: # Gotta Catch 'Em All self.information.additional_info = "" async def get_releases(self): """Get repository releases.""" if self.status.show_beta: temp = await self.repository_object.get_releases(prerelease=True) else: temp = await self.repository_object.get_releases(prerelease=False) if not temp: return self.releases.releases = True self.releases.published_tags = [] for release in temp: self.releases.published_tags.append(release.tag_name) self.releases.last_release_object = temp[0] if self.status.selected_tag is not None: if self.status.selected_tag != self.information.default_branch: for release in temp: if release.tag_name == self.status.selected_tag: self.releases.last_release_object = release break self.versions.available = temp[0].tag_name def remove(self): """Run remove tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Starting removal") if self.information.uid in self.common.installed: self.common.installed.remove(self.information.uid) for repository in self.repositories: if repository.information.uid == self.information.uid: self.repositories.remove(repository) async def uninstall(self): """Run uninstall tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Uninstalling") await self.remove_local_directory() self.status.installed = False if self.information.category == "integration": if self.config_flow: await self.reload_custom_components() else: self.pending_restart = True if self.information.full_name in self.common.installed: self.common.installed.remove(self.information.full_name) self.versions.installed = None self.versions.installed_commit = None async def remove_local_directory(self): """Check the local directory.""" import os import shutil from asyncio import sleep try: if self.information.category == "python_script": local_path = "{}/{}.py".format(self.content.path.local, self.information.name) elif self.information.category == "theme": local_path = "{}/{}.yaml".format(self.content.path.local, self.information.name) else: local_path = self.content.path.local if os.path.exists(local_path): self.logger.debug(f"Removing {local_path}") if self.information.category in ["python_script", "theme"]: os.remove(local_path) else: shutil.rmtree(local_path) while os.path.exists(local_path): await sleep(1) except Exception as exception: self.logger.debug(f"Removing {local_path} failed with {exception}") return
class HacsData: """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") self.hacs = get_hacs() async def async_write(self): """Write content to the store files.""" if self.hacs.system.status.background_task or self.hacs.system.disabled: return self.logger.debug("Saving data") # Hacs await async_save_to_store( self.hacs.hass, "hacs", { "view": self.hacs.configuration.frontend_mode, "compact": self.hacs.configuration.frontend_compact, "onboarding_done": self.hacs.configuration.onboarding_done, }, ) # Repositories content = {} for repository in self.hacs.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None data = { "authors": repository.data.authors, "category": repository.data.category, "description": repository.data.description, "domain": repository.data.domain, "downloads": repository.data.downloads, "full_name": repository.data.full_name, "first_install": repository.status.first_install, "installed_commit": repository.data.installed_commit, "installed": repository.data.installed, "last_commit": repository.data.last_commit, "last_release_tag": repository.data.last_version, "last_updated": repository.data.last_updated, "name": repository.data.name, "new": repository.data.new, "repository_manifest": repository_manifest, "selected_tag": repository.data.selected_tag, "show_beta": repository.data.show_beta, "stars": repository.data.stargazers_count, "topics": repository.data.topics, "version_installed": repository.data.installed_version, } if data: if repository.data.installed and ( repository.data.installed_commit or repository.data.installed_version): await async_save_to_store( self.hacs.hass, f"hacs/{repository.data.id}.hacs", repository.data.to_json(), ) content[str(repository.data.id)] = data await async_save_to_store(self.hacs.hass, "repositories", content) self.hacs.hass.bus.async_fire("hacs/repository", {}) self.hacs.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hacs.hass, "hacs") repositories = await async_load_from_store(self.hacs.hass, "repositories") try: if not hacs and not repositories: # Assume new install self.hacs.system.status.new = True return True self.logger.info("Restore started") self.hacs.system.status.new = False # Hacs self.hacs.configuration.frontend_mode = hacs.get("view", "Grid") self.hacs.configuration.frontend_compact = hacs.get( "compact", False) self.hacs.configuration.onboarding_done = hacs.get( "onboarding_done", False) # Repositories for entry in repositories: repo = repositories[entry] if not self.hacs.is_known(entry): await register_repository(repo["full_name"], repo["category"], False) repository = [ x for x in self.hacs.repositories if str(x.data.id) == str(entry) or x.data.full_name == repo["full_name"] ] if not repository: self.logger.error( f"Did not find {repo['full_name']} ({entry})") continue repository = repository[0] # Restore repository attributes repository.data.id = entry await self.hacs.hass.async_add_executor_job( restore_repository_data, repository, repo) restored = await async_load_from_store(self.hacs.hass, f"hacs/{entry}.hacs") if restored: repository.data.update_data(restored) if not repository.data.installed: repository.logger.debug( "Should be installed but is not... Fixing that!") repository.data.installed = True self.logger.info("Restore done") except Exception as exception: # pylint: disable=broad-except self.logger.critical(f"[{exception}] Restore Failed!") return False return True
class HacsData(Hacs): """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") async def async_write(self): """Write content to the store files.""" if self.system.status.background_task: return self.logger.debug("Saving data") # Hacs await async_save_to_store(self.hass, "hacs", {"view": self.configuration.frontend_mode}) # Repositories content = {} for repository in self.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.information.authors, "topics": repository.information.topics, "category": repository.information.category, "description": repository.information.description, "full_name": repository.information.full_name, "hide": repository.status.hide, "installed_commit": repository.versions.installed_commit, "installed": repository.status.installed, "last_commit": repository.versions.available_commit, "last_release_tag": repository.versions.available, "repository_manifest": repository_manifest, "name": repository.information.name, "new": repository.status.new, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "version_installed": repository.versions.installed, } await async_save_to_store(self.hass, "repositories", content) self.hass.bus.async_fire("hacs/repository", {}) self.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hass, "hacs") repositories = await async_load_from_store(self.hass, "repositories") try: if not hacs and not repositories: # Assume new install return True self.logger.info("Restore started") # Hacs self.configuration.frontend_mode = hacs.get("view", "Grid") # Repositories repositories = repositories for entry in repositories: repo = repositories[entry] if repo["full_name"] == "custom-components/hacs": # Skip the old repo location continue if not self.is_known(repo["full_name"]): await self.register_repository(repo["full_name"], repo["category"], False) repository = self.get_by_name(repo["full_name"]) if repository is None: self.logger.error(f"Did not find {repo['full_name']}") continue # Restore repository attributes repository.information.uid = entry await self.hass.async_add_executor_job(restore_repository_data, repository, repo) self.logger.info("Restore done") except Exception as exception: # pylint: disable=broad-except self.logger.critical(f"[{exception}] Restore Failed!") return False return True
class HacsData: """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") self.hacs = get_hacs() async def async_write(self): """Write content to the store files.""" if self.hacs.system.status.background_task or self.hacs.system.disabled: return self.logger.debug("Saving data") # Hacs await async_save_to_store( self.hacs.hass, "hacs", { "view": self.hacs.configuration.frontend_mode, "compact": self.hacs.configuration.frontend_compact, "onboarding_done": self.hacs.configuration.onboarding_done, }, ) await async_save_to_store(self.hacs.hass, "removed", [x.__dict__ for x in removed_repositories]) # Repositories content = {} for repository in self.hacs.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.data.authors, "category": repository.data.category, "description": repository.data.description, "downloads": repository.releases.downloads, "full_name": repository.data.full_name, "first_install": repository.status.first_install, "hide": repository.status.hide, "installed_commit": repository.versions.installed_commit, "installed": repository.status.installed, "last_commit": repository.versions.available_commit, "last_release_tag": repository.versions.available, "last_updated": repository.information.last_updated, "name": repository.data.name, "new": repository.status.new, "repository_manifest": repository_manifest, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "stars": repository.data.stargazers_count, "topics": repository.data.topics, "version_installed": repository.versions.installed, } await async_save_to_store(self.hacs.hass, "repositories", content) self.hacs.hass.bus.async_fire("hacs/repository", {}) self.hacs.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hacs.hass, "hacs") repositories = await async_load_from_store(self.hacs.hass, "repositories") removed = await async_load_from_store(self.hacs.hass, "removed") try: if not hacs and not repositories: # Assume new install self.hacs.system.status.new = True return True self.logger.info("Restore started") # Hacs self.hacs.configuration.frontend_mode = hacs.get("view", "Grid") self.hacs.configuration.frontend_compact = hacs.get( "compact", False) self.hacs.configuration.onboarding_done = hacs.get( "onboarding_done", False) for entry in removed: removed_repo = get_removed(entry["repository"]) removed_repo.update_data(entry) # Repositories for entry in repositories: repo = repositories[entry] if not self.hacs.is_known(repo["full_name"]): await register_repository(repo["full_name"], repo["category"], False) repository = self.hacs.get_by_name(repo["full_name"]) if repository is None: self.logger.error(f"Did not find {repo['full_name']}") continue # Restore repository attributes repository.information.uid = entry await self.hacs.hass.async_add_executor_job( restore_repository_data, repository, repo) self.logger.info("Restore done") except Exception as exception: # pylint: disable=broad-except self.logger.critical(f"[{exception}] Restore Failed!") return False return True
class HacsWebResponse(HomeAssistantView, Hacs): """Base View Class for HACS.""" requires_auth = False name = "hacs" def __init__(self): """Initialize.""" self.logger = Logger("hacs.http") self.url = self.hacsweb + "/{path:.+}" self.endpoint = None self.postdata = None self.raw_headers = None self.repository_id = None self.request = None self.requested_file = None async def get(self, request, path): # pylint: disable=unused-argument """Handle HACS Web requests.""" if self.system.disabled: return web.Response(status=404) self.endpoint = path.split("/")[0] self.raw_headers = request.raw_headers self.request = request self.requested_file = path.replace(self.endpoint + "/", "") self.repository_id = path.replace(self.endpoint + "/", "") if self.endpoint != "static": self.logger.debug(f"Endpoint ({self.endpoint}) called") if self.endpoint in WEBRESPONSE: try: response = WEBRESPONSE[self.endpoint] response = await response.response(self) except Exception as exception: render = self.render("error", message=exception) return web.Response(body=render, content_type="text/html", charset="utf-8") else: # Return default response. response = await WEBRESPONSE["generic"].response(self) # set headers response.headers[ "Cache-Control"] = "no-cache, must-revalidate, s-max_age=0" response.headers["Pragma"] = "no-cache" # serve the response return response def render(self, templatefile, location=None, repository=None, message=None): """Render a template file.""" loader = Environment( loader=PackageLoader("custom_components.hacs.frontend")) template = loader.get_template(templatefile + ".html") return template.render({ "hacs": self, "location": location, "repository": repository, "message": message, "timestamp": time(), })
class HacsRepository(Hacs): """HacsRepository.""" def __init__(self): """Set up HacsRepository.""" self.data = {} self.content = RepositoryContent() self.content.path = RepositoryPath() self.information = RepositoryInformation() self.repository_object = None self.status = RepositoryStatus() self.state = None self.manifest = {} self.repository_manifest = HacsManifest.from_dict({}) self.validate = Validate() self.releases = RepositoryReleases() self.versions = RepositoryVersions() self.pending_restart = False self.logger = None self.tree = [] self.treefiles = [] self.ref = None @property def pending_upgrade(self): """Return pending upgrade.""" if self.status.installed: if self.status.selected_tag is not None: if self.status.selected_tag == self.information.default_branch: if self.versions.installed_commit != self.versions.available_commit: return True return False if self.display_installed_version != self.display_available_version: return True return False @property def config_flow(self): """Return bool if integration has config_flow.""" if self.manifest: if self.information.full_name == "hacs/integration": return False return self.manifest.get("config_flow", False) return False @property def custom(self): """Return flag if the repository is custom.""" if self.information.full_name.split("/")[0] in [ "custom-components", "custom-cards", ]: return False if self.information.full_name in self.common.default: return False if self.information.full_name == "hacs/integration": return False return True @property def can_install(self): """Return bool if repository can be installed.""" target = None if self.information.homeassistant_version is not None: target = self.information.homeassistant_version if self.repository_manifest is not None: if self.repository_manifest.homeassistant is not None: target = self.repository_manifest.homeassistant if target is not None: if self.releases.releases: if not version_left_higher_then_right(self.system.ha_version, target): return False return True @property def display_name(self): """Return display name.""" return get_repository_name( self.repository_manifest, self.information.name, self.information.category, self.manifest, ) @property def display_status(self): """Return display_status.""" if self.status.new: status = "new" elif self.pending_restart: status = "pending-restart" elif self.pending_upgrade: status = "pending-upgrade" elif self.status.installed: status = "installed" else: status = "default" return status @property def display_status_description(self): """Return display_status_description.""" description = { "default": "Not installed.", "pending-restart": "Restart pending.", "pending-upgrade": "Upgrade pending.", "installed": "No action required.", "new": "This is a newly added repository.", } return description[self.display_status] @property def display_installed_version(self): """Return display_authors""" if self.versions.installed is not None: installed = self.versions.installed else: if self.versions.installed_commit is not None: installed = self.versions.installed_commit else: installed = "" return installed @property def display_available_version(self): """Return display_authors""" if self.versions.available is not None: available = self.versions.available else: if self.versions.available_commit is not None: available = self.versions.available_commit else: available = "" return available @property def display_version_or_commit(self): """Does the repositoriy use releases or commits?""" if self.releases.releases: version_or_commit = "version" else: version_or_commit = "commit" return version_or_commit @property def main_action(self): """Return the main action.""" actions = { "new": "INSTALL", "default": "INSTALL", "installed": "REINSTALL", "pending-restart": "REINSTALL", "pending-upgrade": "UPGRADE", } return actions[self.display_status] async def common_validate(self): """Common validation steps of the repository.""" # Attach helpers self.validate.errors = [] self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) if self.ref is None: self.ref = version_to_install(self) # Step 1: Make sure the repository exist. self.logger.debug("Checking repository.") try: self.repository_object = await self.github.get_repo( self.information.full_name) self.data = self.repository_object.attributes except Exception as exception: # Gotta Catch 'Em All if not self.system.status.startup: self.logger.error(exception) self.validate.errors.append("Repository does not exist.") return if not self.tree: self.tree = await self.repository_object.get_tree(self.ref) self.treefiles = [] for treefile in self.tree: self.treefiles.append(treefile.full_path) # Step 2: Make sure the repository is not archived. if self.repository_object.archived: self.validate.errors.append("Repository is archived.") return # Step 3: Make sure the repository is not in the blacklist. if self.information.full_name in self.common.blacklist: self.validate.errors.append("Repository is in the blacklist.") return # Step 4: default branch self.information.default_branch = self.repository_object.default_branch # Step 5: Get releases. await self.get_releases() # Step 6: Get the content of hacs.json await self.get_repository_manifest_content() # Set repository name self.information.name = self.information.full_name.split("/")[1] async def common_registration(self): """Common registration steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository if self.repository_object is None: self.repository_object = await self.github.get_repo( self.information.full_name) # Set id self.information.uid = str(self.repository_object.id) # Set topics self.information.topics = self.repository_object.topics # Set stargazers_count self.information.stars = self.repository_object.attributes.get( "stargazers_count", 0) # Set description if self.repository_object.description: self.information.description = self.repository_object.description async def common_update(self): """Common information update steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.debug("Getting repository information") # Set ref self.ref = version_to_install(self) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update tree self.tree = await self.repository_object.get_tree(self.ref) self.treefiles = [] for treefile in self.tree: self.treefiles.append(treefile.full_path) # Update description if self.repository_object.description: self.information.description = self.repository_object.description # Set stargazers_count self.information.stars = self.repository_object.attributes.get( "stargazers_count", 0) # Update default branch self.information.default_branch = self.repository_object.default_branch # Update last updaeted self.information.last_updated = self.repository_object.attributes.get( "pushed_at", 0) # Update topics self.information.topics = self.repository_object.topics # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" self.information.additional_info = await get_info_md_content(self) # Update releases await self.get_releases() async def install(self): """Common installation steps of the repository.""" await install_repository(self) async def download_zip(self, validate): """Download ZIP archive from repository release.""" try: contents = False for release in self.releases.objects: self.logger.info( f"ref: {self.ref} --- tag: {release.tag_name}") if release.tag_name == self.ref.split("/")[1]: contents = release.assets if not contents: return validate for content in contents or []: filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue result = await async_save_file( f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", filecontent, ) with zipfile.ZipFile( f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r") as zip_file: zip_file.extractall(self.content.path.local) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception: validate.errors.append(f"Download was not complete.") return validate async def download_content(self, validate, directory_path, local_directory, ref): """Download the content of a directory.""" from custom_components.hacs.helpers.download import download_content validate = await download_content(self, validate, local_directory) return validate async def get_repository_manifest_content(self): """Get the content of the hacs.json file.""" if self.ref is None: self.ref = version_to_install(self) try: manifest = await self.repository_object.get_contents( "hacs.json", self.ref) self.repository_manifest = HacsManifest.from_dict( json.loads(manifest.content)) except (AIOGitHubException, Exception): # Gotta Catch 'Em All pass async def get_releases(self): """Get repository releases.""" if self.status.show_beta: self.releases.objects = await self.repository_object.get_releases( prerelease=True, returnlimit=self.configuration.release_limit) else: self.releases.objects = await self.repository_object.get_releases( prerelease=False, returnlimit=self.configuration.release_limit) if not self.releases.objects: return self.releases.releases = True self.releases.published_tags = [] for release in self.releases.objects: self.releases.published_tags.append(release.tag_name) self.releases.last_release_object = self.releases.objects[0] if self.status.selected_tag is not None: if self.status.selected_tag != self.information.default_branch: for release in self.releases.objects: if release.tag_name == self.status.selected_tag: self.releases.last_release_object = release break if self.releases.last_release_object.assets: self.releases.last_release_object_downloads = self.releases.last_release_object.assets[ 0].attributes.get("download_count") self.versions.available = self.releases.objects[0].tag_name def remove(self): """Run remove tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Starting removal") if self.information.uid in self.common.installed: self.common.installed.remove(self.information.uid) for repository in self.repositories: if repository.information.uid == self.information.uid: self.repositories.remove(repository) async def uninstall(self): """Run uninstall tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Uninstalling") await self.remove_local_directory() self.status.installed = False if self.information.category == "integration": if self.config_flow: await self.reload_custom_components() else: self.pending_restart = True elif self.information.category == "theme": try: await self.hass.services.async_call("frontend", "reload_themes", {}) except Exception: # pylint: disable=broad-except pass if self.information.full_name in self.common.installed: self.common.installed.remove(self.information.full_name) self.versions.installed = None self.versions.installed_commit = None self.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "uninstall", "repository": self.information.full_name, }, ) async def remove_local_directory(self): """Check the local directory.""" import shutil from asyncio import sleep try: if self.information.category == "python_script": local_path = "{}/{}.py".format(self.content.path.local, self.information.name) elif self.information.category == "theme": local_path = "{}/{}.yaml".format(self.content.path.local, self.information.name) else: local_path = self.content.path.local if os.path.exists(local_path): self.logger.debug(f"Removing {local_path}") if self.information.category in ["python_script", "theme"]: os.remove(local_path) else: shutil.rmtree(local_path) while os.path.exists(local_path): await sleep(1) except Exception as exception: self.logger.debug(f"Removing {local_path} failed with {exception}") return
class HacsData(Hacs): """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") def check_corrupted_files(self): """Return True if one (or more) of the files are corrupted.""" for store in STORES: path = f"{self.system.config_path}/.storage/{STORES[store]}" if os.path.exists(path): if os.stat(path).st_size == 0: # File is empty (corrupted) return True return False def read(self, store): """Return data from a store.""" path = f"{self.system.config_path}/.storage/{STORES[store]}" content = None if os.path.exists(path): with open(path, "r", encoding="utf-8") as storefile: content = storefile.read() content = json.loads(content) return content def write(self): """Write content to the store files.""" if self.system.status.background_task: return self.logger.debug("Saving data") # Hacs path = f"{self.system.config_path}/.storage/{STORES['hacs']}" hacs = {"view": self.configuration.frontend_mode} save(self.logger, path, hacs) # Installed path = f"{self.system.config_path}/.storage/{STORES['installed']}" installed = {} for repository_name in self.common.installed: repository = self.get_by_name(repository_name) if repository is None: self.logger.warning( f"Did not save information about {repository_name}") continue installed[repository.information.full_name] = { "version_type": repository.display_version_or_commit, "version_installed": repository.display_installed_version, "version_available": repository.display_available_version, } save(self.logger, path, installed) # Repositories path = f"{self.system.config_path}/.storage/{STORES['repositories']}" content = {} for repository in self.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.information.authors, "topics": repository.information.topics, "category": repository.information.category, "description": repository.information.description, "full_name": repository.information.full_name, "hide": repository.status.hide, "installed_commit": repository.versions.installed_commit, "installed": repository.status.installed, "last_commit": repository.versions.available_commit, "last_release_tag": repository.versions.available, "repository_manifest": repository_manifest, "name": repository.information.name, "new": repository.status.new, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "version_installed": repository.versions.installed, } # Validate installed repositories count_installed = len(installed) + 1 # For HACS it self count_installed_restore = 0 for repository in self.repositories: if repository.status.installed: count_installed_restore += 1 if count_installed < count_installed_restore: self.logger.debug("Save failed!") self.logger.debug( f"Number of installed repositories does not match the number of stored repositories [{count_installed} vs {count_installed_restore}]" ) return save(self.logger, path, content) async def restore(self): """Restore saved data.""" try: hacs = self.read("hacs") installed = self.read("installed") repositrories = self.read("repositories") if self.check_corrupted_files(): # Coruptted installation self.logger.critical( "Restore failed one or more files are corrupted!") return False if hacs is None and installed is None and repositrories is None: # Assume new install return True self.logger.info("Restore started") # Hacs hacs = hacs["data"] self.configuration.frontend_mode = hacs["view"] # Installed installed = installed["data"] for repository in installed: self.common.installed.append(repository) # Repositories repositrories = repositrories["data"] for entry in repositrories: repo = repositrories[entry] if not self.is_known(repo["full_name"]): await self.register_repository(repo["full_name"], repo["category"], False) repository = self.get_by_name(repo["full_name"]) if repository is None: self.logger.error(f"Did not find {repo['full_name']}") continue # Restore repository attributes if repo.get("authors") is not None: repository.information.authors = repo["authors"] if repo.get("topics", []): repository.information.topics = repo["topics"] if repo.get("description") is not None: repository.information.description = repo["description"] if repo.get("name") is not None: repository.information.name = repo["name"] if repo.get("hide") is not None: repository.status.hide = repo["hide"] if repo.get("installed") is not None: repository.status.installed = repo["installed"] if repository.status.installed: repository.status.first_install = False if repo.get("selected_tag") is not None: repository.status.selected_tag = repo["selected_tag"] if repo.get("repository_manifest") is not None: repository.repository_manifest = HacsManifest( repo["repository_manifest"]) if repo.get("show_beta") is not None: repository.status.show_beta = repo["show_beta"] if repo.get("last_commit") is not None: repository.versions.available_commit = repo["last_commit"] repository.information.uid = entry if repo.get("last_release_tag") is not None: repository.releases.last_release = repo["last_release_tag"] repository.versions.available = repo["last_release_tag"] if repo.get("new") is not None: repository.status.new = repo["new"] if repo["full_name"] == "custom-components/hacs": repository.versions.installed = VERSION repository.status.installed = True if "b" in VERSION: repository.status.show_beta = True elif repo.get("version_installed") is not None: repository.versions.installed = repo["version_installed"] if repo.get("installed_commit") is not None: repository.versions.installed_commit = repo[ "installed_commit"] if repo["full_name"] in self.common.installed: repository.status.installed = True repository.status.new = False frominstalled = installed[repo["full_name"]] if frominstalled["version_type"] == "commit": repository.versions.installed_commit = frominstalled[ "version_installed"] repository.versions.available_commit = frominstalled[ "version_available"] else: repository.versions.installed = frominstalled[ "version_installed"] repository.versions.available = frominstalled[ "version_available"] # Check the restore. count_installed = len(installed) + 1 # For HACS it self count_installed_restore = 0 installed_restore = [] for repository in self.repositories: if repository.status.installed: installed_restore.append(repository.information.full_name) if (repository.information.full_name not in self.common.installed and repository.information.full_name != "custom-components/hacs"): self.logger.warning( f"{repository.information.full_name} is not in common.installed" ) count_installed_restore += 1 if count_installed < count_installed_restore: for repo in installed: installed_restore.remove(repo) self.logger.warning(f"Check {repo}") self.logger.critical("Restore failed!") self.logger.critical( f"Number of installed repositories does not match the number of restored repositories [{count_installed} vs {count_installed_restore}]" ) return False self.logger.info("Restore done") except Exception as exception: self.logger.critical(f"[{exception}] Restore Failed!") return False return True
class HacsRepository(Hacs): """HacsRepository.""" def __init__(self): """Set up HacsRepository.""" self.content = RepositoryContent() self.content.path = RepositoryPath() self.information = RepositoryInformation() self.repository_object = None self.status = RepositoryStatus() self.state = None self.manifest = {} self.repository_manifest = HacsManifest.from_dict({}) self.validate = Validate() self.releases = RepositoryReleases() self.versions = RepositoryVersions() self.pending_restart = False self.logger = None @property def pending_upgrade(self): """Return pending upgrade.""" if self.status.installed: if self.display_installed_version != self.display_available_version: return True return False @property def ref(self): """Return the ref.""" if self.status.selected_tag is not None: if self.status.selected_tag == self.information.default_branch: return self.information.default_branch return "tags/{}".format(self.status.selected_tag) if self.releases.releases: return "tags/{}".format(self.versions.available) return self.information.default_branch @property def custom(self): """Return flag if the repository is custom.""" if self.information.full_name.split("/")[0] in [ "custom-components", "custom-cards", ]: return False if self.information.full_name in self.common.default: return False if self.information.full_name == "hacs/integration": return False return True @property def can_install(self): """Return bool if repository can be installed.""" target = None if self.information.homeassistant_version is not None: target = self.information.homeassistant_version if self.repository_manifest is not None: if self.repository_manifest.homeassistant is not None: target = self.repository_manifest.homeassistant if target is not None: if self.releases.releases: if not version_left_higher_then_right(self.system.ha_version, target): return False return True @property def display_name(self): """Return display name.""" return get_repository_name( self.repository_manifest, self.information.name, self.information.category, self.manifest, ) @property def display_status(self): """Return display_status.""" if self.status.new: status = "new" elif self.pending_restart: status = "pending-restart" elif self.pending_upgrade: status = "pending-upgrade" elif self.status.installed: status = "installed" else: status = "default" return status @property def display_status_description(self): """Return display_status_description.""" description = { "default": "Not installed.", "pending-restart": "Restart pending.", "pending-upgrade": "Upgrade pending.", "installed": "No action required.", "new": "This is a newly added repository.", } return description[self.display_status] @property def display_installed_version(self): """Return display_authors""" if self.versions.installed is not None: installed = self.versions.installed else: if self.versions.installed_commit is not None: installed = self.versions.installed_commit else: installed = "" return installed @property def display_available_version(self): """Return display_authors""" if self.versions.available is not None: available = self.versions.available else: if self.versions.available_commit is not None: available = self.versions.available_commit else: available = "" return available @property def display_version_or_commit(self): """Does the repositoriy use releases or commits?""" if self.releases.releases: version_or_commit = "version" else: version_or_commit = "commit" return version_or_commit @property def main_action(self): """Return the main action.""" actions = { "new": "INSTALL", "default": "INSTALL", "installed": "REINSTALL", "pending-restart": "REINSTALL", "pending-upgrade": "UPGRADE", } return actions[self.display_status] async def common_validate(self): """Common validation steps of the repository.""" # Attach helpers self.validate.errors = [] self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Step 1: Make sure the repository exist. self.logger.debug("Checking repository.") try: self.repository_object = await self.github.get_repo( self.information.full_name) except Exception as exception: # Gotta Catch 'Em All if not self.system.status.startup: self.logger.error(exception) self.validate.errors.append("Repository does not exist.") return # Step 2: Make sure the repository is not archived. if self.repository_object.archived: self.validate.errors.append("Repository is archived.") return # Step 3: Make sure the repository is not in the blacklist. if self.information.full_name in self.common.blacklist: self.validate.errors.append("Repository is in the blacklist.") return # Step 4: default branch self.information.default_branch = self.repository_object.default_branch # Step 5: Get releases. await self.get_releases() # Step 6: Get the content of hacs.json await self.get_repository_manifest_content() # Set repository name self.information.name = self.information.full_name.split("/")[1] async def common_registration(self): """Common registration steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository if self.repository_object is None: self.repository_object = await self.github.get_repo( self.information.full_name) # Set id self.information.uid = str(self.repository_object.id) # Set topics self.information.topics = self.repository_object.topics # Set stargazers_count self.information.stars = self.repository_object.attributes.get( "stargazers_count", 0) # Set description if self.repository_object.description: self.information.description = self.repository_object.description async def common_update(self): """Common information update steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update description if self.repository_object.description: self.information.description = self.repository_object.description # Set stargazers_count self.information.stars = self.repository_object.attributes.get( "stargazers_count", 0) # Update default branch self.information.default_branch = self.repository_object.default_branch # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Update last updaeted self.information.last_updated = self.repository_object.pushed_at # Update topics self.information.topics = self.repository_object.topics # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" await self.get_info_md_content() # Update releases await self.get_releases() async def install(self): """Common installation steps of the repository.""" self.validate.errors = [] persistent_directory = None await self.update_repository() if self.repository_manifest: if self.repository_manifest.persistent_directory: if os.path.exists( f"{self.content.path.local}/{self.repository_manifest.persistent_directory}" ): persistent_directory = Backup( f"{self.content.path.local}/{self.repository_manifest.persistent_directory}", tempfile.TemporaryFile() + "/hacs_persistent_directory/", ) persistent_directory.create() if self.status.installed and not self.content.single: backup = Backup(self.content.path.local) backup.create() if self.repository_manifest.zip_release: validate = await self.download_zip(self.validate) else: validate = await self.download_content( self.validate, self.content.path.remote, self.content.path.local, self.ref, ) if validate.errors: for error in validate.errors: self.logger.error(error) if self.status.installed and not self.content.single: backup.restore() if self.status.installed and not self.content.single: backup.cleanup() if persistent_directory is not None: persistent_directory.restore() persistent_directory.cleanup() if validate.success: if self.information.full_name not in self.common.installed: if self.information.full_name == "hacs/integration": self.common.installed.append(self.information.full_name) self.status.installed = True self.versions.installed_commit = self.versions.available_commit if self.status.selected_tag is not None: self.versions.installed = self.status.selected_tag else: self.versions.installed = self.versions.available if self.information.category == "integration": if (self.config_flow and self.information.full_name != "hacs/integration"): await self.reload_custom_components() self.pending_restart = True elif self.information.category == "theme": try: await self.hass.services.async_call( "frontend", "reload_themes", {}) except Exception: # pylint: disable=broad-except pass self.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "install", "repository": self.information.full_name, }, ) async def download_zip(self, validate): """Download ZIP archive from repository release.""" try: contents = False for release in self.releases.objects: self.logger.info( f"ref: {self.ref} --- tag: {release.tag_name}") if release.tag_name == self.ref.split("/")[1]: contents = release.assets if not contents: return validate for content in contents or []: filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue result = await async_save_file( f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", filecontent, ) with zipfile.ZipFile( f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r") as zip_file: zip_file.extractall(self.content.path.local) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception: validate.errors.append(f"Download was not complete.") return validate async def download_content(self, validate, directory_path, local_directory, ref): """Download the content of a directory.""" try: # Get content contents = [] if self.releases.releases: for release in self.releases.objects: if self.status.selected_tag == release.tag_name: contents = release.assets if not contents: if self.content.single: contents = self.content.objects else: contents = await self.repository_object.get_contents( directory_path, self.ref) for content in contents: if content.type == "dir" and ( self.repository_manifest.content_in_root or self.content.path.remote != ""): await self.download_content(validate, content.path, local_directory, ref) continue if self.information.category == "plugin": if not content.name.endswith(".js"): if self.content.path.remote != "dist": continue self.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue # Save the content of the file. if self.content.single: local_directory = self.content.path.local else: _content_path = content.path if not self.repository_manifest.content_in_root: _content_path = _content_path.replace( f"{self.content.path.remote}/", "") local_directory = f"{self.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = f"{local_directory}/{content.name}" result = await async_save_file(local_file_path, filecontent) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception: validate.errors.append(f"Download was not complete.") return validate async def get_repository_manifest_content(self): """Get the content of the hacs.json file.""" try: manifest = await self.repository_object.get_contents( "hacs.json", self.ref) self.repository_manifest = HacsManifest.from_dict( json.loads(manifest.content)) except (AIOGitHubException, Exception): # Gotta Catch 'Em All pass async def get_info_md_content(self): """Get the content of info.md""" from ..handler.template import render_template info = None info_files = ["info", "info.md"] if self.repository_manifest is not None: if self.repository_manifest.render_readme: info_files = ["readme", "readme.md"] try: root = await self.repository_object.get_contents("", self.ref) for file in root: if file.name.lower() in info_files: info = await self.repository_object.get_contents( file.name, self.ref) break if info is None: self.information.additional_info = "" else: info = info.content.replace("<svg", "<disabled").replace( "</svg", "</disabled") info = info.replace( '<a href="http', '<a rel="noreferrer" target="_blank" href="http') self.information.additional_info = render_template(info, self) except (AIOGitHubException, Exception): self.information.additional_info = "" async def get_releases(self): """Get repository releases.""" if self.status.show_beta: self.releases.objects = await self.repository_object.get_releases( prerelease=True, returnlimit=self.configuration.release_limit) else: self.releases.objects = await self.repository_object.get_releases( prerelease=False, returnlimit=self.configuration.release_limit) if not self.releases.objects: return self.releases.releases = True self.releases.published_tags = [] for release in self.releases.objects: self.releases.published_tags.append(release.tag_name) self.releases.last_release_object = self.releases.objects[0] if self.status.selected_tag is not None: if self.status.selected_tag != self.information.default_branch: for release in self.releases.objects: if release.tag_name == self.status.selected_tag: self.releases.last_release_object = release break self.versions.available = self.releases.objects[0].tag_name def remove(self): """Run remove tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Starting removal") if self.information.uid in self.common.installed: self.common.installed.remove(self.information.uid) for repository in self.repositories: if repository.information.uid == self.information.uid: self.repositories.remove(repository) async def uninstall(self): """Run uninstall tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Uninstalling") await self.remove_local_directory() self.status.installed = False if self.information.category == "integration": if self.config_flow: await self.reload_custom_components() else: self.pending_restart = True elif self.information.category == "theme": try: await self.hass.services.async_call("frontend", "reload_themes", {}) except Exception: # pylint: disable=broad-except pass if self.information.full_name in self.common.installed: self.common.installed.remove(self.information.full_name) self.versions.installed = None self.versions.installed_commit = None self.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "uninstall", "repository": self.information.full_name, }, ) async def remove_local_directory(self): """Check the local directory.""" import shutil from asyncio import sleep try: if self.information.category == "python_script": local_path = "{}/{}.py".format(self.content.path.local, self.information.name) elif self.information.category == "theme": local_path = "{}/{}.yaml".format(self.content.path.local, self.information.name) else: local_path = self.content.path.local if os.path.exists(local_path): self.logger.debug(f"Removing {local_path}") if self.information.category in ["python_script", "theme"]: os.remove(local_path) else: shutil.rmtree(local_path) while os.path.exists(local_path): await sleep(1) except Exception as exception: self.logger.debug(f"Removing {local_path} failed with {exception}") return