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.error(msg)
        return False

    return os.path.exists(location)
Beispiel #2
0
def internet_connectivity_check(host="api.github.com"):
    """Verify network connectivity."""
    logger = Logger("hacs.network.check")
    try:
        result = ping(host, count=1, timeout=3)
        if result.success():
            logger.info("All good")
            return True
    except gaierror:
        logger.error(f"DNS issues, could not resolve {host}")
    return False
Beispiel #3
0
async def get_default_repos_lists(session, token, default: str) -> dict:
    """Gets repositories from default list."""
    repositories = []
    logger = Logger("hacs")

    try:
        repo = await get_repository(session, token, "hacs/default")
        content = await repo.get_contents(default)
        repositories = json.loads(content.content)

    except AIOGitHubAPIException as exception:
        logger.error(exception)

    return repositories
Beispiel #4
0
async def get_default_repos_lists(github: type(AIOGitHub), default: str) -> dict:
    """Gets repositories from default list."""
    repositories = []
    logger = Logger("hacs")

    try:
        repo = await github.get_repo("hacs/default")
        content = await repo.get_contents(default)
        repositories = json.loads(content.content)

    except AIOGitHubException as exception:
        logger.error(exception)

    return repositories
Beispiel #5
0
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)
Beispiel #6
0
async def get_default_repos_orgs(github: type(GitHub), category: str) -> dict:
    """Gets default org repositories."""
    repositories = []
    logger = Logger("hacs")
    orgs = {
        "plugin": "custom-cards",
        "integration": "custom-components",
        "theme": "home-assistant-community-themes",
    }
    if category not in orgs:
        return repositories

    try:
        repos = await github.get_org_repos(orgs[category])
        for repo in repos:
            repositories.append(repo.full_name)

    except AIOGitHubAPIException as exception:
        logger.error(exception)

    return repositories
Beispiel #7
0
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(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
Beispiel #9
0
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
Beispiel #10
0
class HacsTheme(HacsRepository):
    """Themes in HACS."""
    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.category = "theme"
        self.content.path.remote = "themes"
        self.content.path.local = f"{self.hacs.system.config_path}/themes/"
        self.content.single = False
        self.logger = Logger(
            f"hacs.repository.{self.data.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith("themes/") and treefile.endswith(".yaml"):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if self.data.content_in_root:
            self.content.path.remote = ""

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self, ref=None):
        """Registration."""
        if ref is not None:
            self.ref = ref
            self.force_branch = True
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set name
        find_file_name(self)
        self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"

    async def update_repository(self, ignore_issues=False):
        """Update."""
        await self.common_update(ignore_issues)

        # Get theme objects.
        if self.data.content_in_root:
            self.content.path.remote = ""

        # Update name
        find_file_name(self)
        self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
Beispiel #11
0
class HacsPlugin(HacsRepository):
    """Plugins in HACS."""
    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.file_name = None
        self.data.category = "plugin"
        self.information.javascript_type = None
        self.content.path.local = (
            f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}"
        )
        self.logger = Logger(
            f"hacs.repository.{self.data.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        find_file_name(self)

        if self.content.path.remote is None:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if self.content.path.remote == "release":
            self.content.single = True

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self, ref=None):
        """Registration."""
        if ref is not None:
            self.ref = ref
            self.force_branch = True
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

    async def update_repository(self, ignore_issues=False):
        """Update."""
        await self.common_update(ignore_issues)

        # Get plugin objects.
        find_file_name(self)

        if self.content.path.remote is None:
            self.validate.errors.append("Repostitory structure not compliant")

        if self.content.path.remote == "release":
            self.content.single = True

    async def get_package_content(self):
        """Get package content."""
        try:
            package = await self.repository_object.get_contents("package.json")
            package = json.loads(package.content)

            if package:
                self.data.authors = package["author"]
        except Exception:  # pylint: disable=broad-except
            pass
Beispiel #12
0
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
Beispiel #13
0
class HacsPlugin(HacsRepository):
    """Plugins in HACS."""
    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.file_name = None
        self.data.category = "plugin"
        self.information.javascript_type = None
        self.content.path.local = (
            f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}"
        )
        self.logger = Logger(
            f"hacs.repository.{self.data.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        find_file_name(self)

        if self.content.path.remote is None:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if self.content.path.remote == "release":
            self.content.single = True

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

    async def update_repository(self):
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        # Run common update steps.
        await self.common_update()

        # Get plugin objects.
        find_file_name(self)

        # Get JS type
        await self.parse_readme_for_jstype()

        if self.content.path.remote is None:
            self.validate.errors.append("Repostitory structure not compliant")

        if self.content.path.remote == "release":
            self.content.single = True

    async def get_package_content(self):
        """Get package content."""
        try:
            package = await self.repository_object.get_contents("package.json")
            package = json.loads(package.content)

            if package:
                self.data.authors = package["author"]
        except Exception:  # pylint: disable=broad-except
            pass

    async def parse_readme_for_jstype(self):
        """Parse the readme looking for js type."""
        readme = None
        readme_files = ["readme", "readme.md"]
        root = await self.repository_object.get_contents("")
        for file in root:
            if file.name.lower() in readme_files:
                readme = await self.repository_object.get_contents(file.name)
                break

        if readme is None:
            return

        readme = readme.content
        for line in readme.splitlines():
            if "type: module" in line:
                self.information.javascript_type = "module"
                break
            elif "type: js" in line:
                self.information.javascript_type = "js"
                break
Beispiel #14
0
class HacsTheme(HacsRepository):
    """Themes in HACS."""

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.category = "theme"
        self.content.path.remote = "themes"
        self.content.path.local = self.localpath
        self.content.single = False
        self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")

    @property
    def localpath(self):
        """Return localpath."""
        return f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"

    async def async_post_installation(self):
        """Run post installation steps."""
        try:
            await self.hacs.hass.services.async_call("frontend", "reload_themes", {})
            self.logger.info("Themes reloaded")
        except Exception:  # pylint: disable=broad-except
            pass

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith("themes/") and treefile.endswith(".yaml"):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if self.data.content_in_root:
            self.content.path.remote = ""

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def async_post_registration(self):
        """Registration."""
        # Set name
        find_file_name(self)
        self.content.path.local = self.localpath

    async def update_repository(self, ignore_issues=False):
        """Update."""
        await self.common_update(ignore_issues)

        # Get theme objects.
        if self.data.content_in_root:
            self.content.path.remote = ""

        # Update name
        find_file_name(self)
        self.content.path.local = self.localpath
Beispiel #15
0
class HacsIntegration(HacsRepository):
    """Integrations in HACS."""
    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.category = "integration"
        self.content.path.remote = "custom_components"
        self.content.path.local = self.localpath
        self.logger = Logger(
            f"hacs.repository.{self.data.category}.{full_name}")

    @property
    def localpath(self):
        """Return localpath."""
        return f"{self.hacs.system.config_path}/custom_components/{self.data.domain}"

    async def validate_repository(self):
        """Validate."""
        await self.common_validate()

        # Custom step 1: Validate content.
        if self.data.content_in_root:
            self.content.path.remote = ""

        if self.content.path.remote == "custom_components":
            name = get_first_directory_in_directory(self.tree,
                                                    "custom_components")
            if name is None:
                raise HacsException(
                    f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
                )
            self.content.path.remote = f"custom_components/{name}"

        try:
            await get_integration_manifest(self)
        except HacsException as exception:
            if self.hacs.action:
                raise HacsException(exception)
            self.logger.error(exception)

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self, ref=None):
        """Registration."""
        if ref is not None:
            self.ref = ref
            self.force_branch = True
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set local path
        self.content.path.local = self.localpath

    async def update_repository(self):
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        await self.common_update()

        if self.data.content_in_root:
            self.content.path.remote = ""

        if self.content.path.remote == "custom_components":
            name = get_first_directory_in_directory(self.tree,
                                                    "custom_components")
            self.content.path.remote = f"custom_components/{name}"

        try:
            await get_integration_manifest(self)
        except HacsException as exception:
            self.logger.error(exception)

        # Set local path
        self.content.path.local = self.localpath

    async def reload_custom_components(self):
        """Reload custom_components (and config flows)in HA."""
        self.logger.info("Reloading custom_component cache")
        del self.hacs.hass.data["custom_components"]
        await async_get_custom_components(self.hacs.hass)
Beispiel #16
0
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("&lt;", "<")
                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
Beispiel #17
0
class HacsNetdaemon(HacsRepository):
    """Netdaemon apps in HACS."""

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.category = "netdaemon"
        self.content.path.local = self.localpath
        self.content.path.remote = "apps"
        self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")

    @property
    def localpath(self):
        """Return localpath."""
        return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}"

    async def validate_repository(self):
        """Validate."""
        await self.common_validate()

        # Custom step 1: Validate content.
        if self.repository_manifest:
            if self.data.content_in_root:
                self.content.path.remote = ""

        if self.content.path.remote == "apps":
            self.data.domain = get_first_directory_in_directory(
                self.tree, self.content.path.remote
            )
            self.content.path.remote = f"apps/{self.data.name}"

        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
                ".cs"
            ):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def update_repository(self, ignore_issues=False):
        """Update."""
        await self.common_update(ignore_issues)

        # Get appdaemon objects.
        if self.repository_manifest:
            if self.data.content_in_root:
                self.content.path.remote = ""

        if self.content.path.remote == "apps":
            self.data.domain = get_first_directory_in_directory(
                self.tree, self.content.path.remote
            )
            self.content.path.remote = f"apps/{self.data.name}"

        # Set local path
        self.content.path.local = self.localpath

    async def async_post_installation(self):
        """Run post installation steps."""
        try:
            await self.hacs.hass.services.async_call(
                "hassio", "addon_restart", {"addon": "c6a2317c_netdaemon"}
            )
        except Exception:  # pylint: disable=broad-except
            pass
Beispiel #18
0
class HacsPlugin(HacsRepository):
    """Plugins in HACS."""

    category = "plugin"

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.information.full_name = full_name
        self.information.category = self.category
        self.information.file_name = None
        self.information.javascript_type = None
        self.content.path.local = (
            f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}"
        )
        self.logger = Logger(f"hacs.repository.{self.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        await self.get_plugin_location()

        if self.content.path.remote is None:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if self.content.path.remote == "release":
            self.content.single = True

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

    async def update_repository(self):
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        # Run common update steps.
        await self.common_update()

        # Get plugin objects.
        await self.get_plugin_location()

        # Get JS type
        await self.parse_readme_for_jstype()

        if self.content.path.remote is None:
            self.validate.errors.append("Repostitory structure not compliant")

        if self.content.path.remote == "release":
            self.content.single = True

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)

    async def get_plugin_location(self):
        """Get plugin location."""
        if self.content.path.remote is not None:
            return

        possible_locations = ["dist", "release", ""]

        if self.data.content_in_root:
            possible_locations = [""]

        for location in possible_locations:
            if self.content.path.remote is not None:
                continue
            try:
                objects = []
                files = []
                if location != "release":
                    try:
                        objects = await self.repository_object.get_contents(
                            location, self.ref
                        )
                    except AIOGitHubException:
                        continue
                else:
                    if self.releases.releases:
                        if self.releases.last_release_object.assets is not None:
                            objects = self.releases.last_release_object.assets

                for item in objects:
                    if item.name.endswith(".js"):
                        files.append(item.name)

                # Handler for plug requirement 3
                valid_filenames = [
                    f"{self.information.name.replace('lovelace-', '')}.js",
                    f"{self.information.name}.js",
                    f"{self.information.name}.umd.js",
                    f"{self.information.name}-bundle.js",
                ]

                if self.repository_manifest:
                    if self.data.filename:
                        valid_filenames.append(self.data.filename)

                for name in valid_filenames:
                    if name in files:
                        # YES! We got it!
                        self.information.file_name = name
                        self.content.path.remote = location
                        self.content.objects = objects
                        self.content.files = files
                        break

            except SystemError:
                pass

    async def get_package_content(self):
        """Get package content."""
        try:
            package = await self.repository_object.get_contents("package.json")
            package = json.loads(package.content)

            if package:
                self.information.authors = package["author"]
        except Exception:  # pylint: disable=broad-except
            pass

    async def parse_readme_for_jstype(self):
        """Parse the readme looking for js type."""
        readme = None
        readme_files = ["readme", "readme.md"]
        root = await self.repository_object.get_contents("")
        for file in root:
            if file.name.lower() in readme_files:
                readme = await self.repository_object.get_contents(file.name)
                break

        if readme is None:
            return

        readme = readme.content
        for line in readme.splitlines():
            if "type: module" in line:
                self.information.javascript_type = "module"
                break
            elif "type: js" in line:
                self.information.javascript_type = "js"
                break
Beispiel #19
0
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
Beispiel #20
0
class HacsPythonScript(HacsRepository):
    """python_scripts in HACS."""

    category = "python_script"

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.information.full_name = full_name
        self.information.category = self.category
        self.content.path.remote = "python_scripts"
        self.content.path.local = f"{self.hacs.system.config_path}/python_scripts"
        self.content.single = True
        self.logger = Logger(f"hacs.repository.{self.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        try:
            self.content.objects = await self.repository_object.get_contents(
                self.content.path.remote, self.ref)
        except AIOGitHubException:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if not isinstance(self.content.objects, list):
            self.validate.errors.append("Repostitory structure not compliant")

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set name
        self.information.name = self.content.objects[0].name.replace(".py", "")

    async def update_repository(self):  # lgtm[py/similar-function]
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        # Run common update steps.
        await self.common_update()

        # Get python_script objects.
        if self.repository_manifest:
            if self.data.content_in_root:
                self.content.path.remote = ""

        self.content.objects = await self.repository_object.get_contents(
            self.content.path.remote, self.ref)

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)

        # Update name
        self.information.name = self.content.objects[0].name.replace(".py", "")

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)
Beispiel #21
0
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
Beispiel #22
0
class HacsAppdaemon(HacsRepository):
    """Appdaemon apps in HACS."""

    category = "appdaemon"

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.information.full_name = full_name
        self.information.category = self.category
        self.content.path.local = self.localpath
        self.content.path.remote = "apps"
        self.logger = Logger(f"hacs.repository.{self.category}.{full_name}")

    @property
    def localpath(self):
        """Return localpath."""
        return f"{self.hacs.system.config_path}/appdaemon/apps/{self.information.name}"

    async def validate_repository(self):
        """Validate."""
        await self.common_validate()

        # Custom step 1: Validate content.
        try:
            addir = await self.repository_object.get_contents("apps", self.ref)
        except AIOGitHubException:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if not isinstance(addir, list):
            self.validate.errors.append("Repostitory structure not compliant")

        self.content.path.remote = addir[0].path
        self.information.name = addir[0].name
        self.content.objects = await self.repository_object.get_contents(
            self.content.path.remote, self.ref)

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set local path
        self.content.path.local = self.localpath

    async def update_repository(self):
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        await self.common_update()

        # Get appdaemon objects.
        if self.repository_manifest:
            if self.data.content_in_root:
                self.content.path.remote = ""

        if self.content.path.remote == "apps":
            addir = await self.repository_object.get_contents(
                self.content.path.remote, self.ref)
            self.content.path.remote = addir[0].path
            self.information.name = addir[0].name
        self.content.objects = await self.repository_object.get_contents(
            self.content.path.remote, self.ref)

        self.content.files = []
        for filename in self.content.objects:
            self.content.files.append(filename.name)

        # Set local path
        self.content.path.local = self.localpath
Beispiel #23
0
class HacsNetdaemon(HacsRepository):
    """Netdaemon apps in HACS."""
    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.category = "netdaemon"
        self.content.path.local = self.localpath
        self.content.path.remote = "apps"
        self.logger = Logger(
            f"hacs.repository.{self.data.category}.{full_name}")

    @property
    def localpath(self):
        """Return localpath."""
        return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}"

    async def validate_repository(self):
        """Validate."""
        await self.common_validate()

        # Custom step 1: Validate content.
        if self.repository_manifest:
            if self.data.content_in_root:
                self.content.path.remote = ""

        if self.content.path.remote == "apps":
            self.data.domain = get_first_directory_in_directory(
                self.tree, self.content.path.remote)
            self.content.path.remote = f"apps/{self.data.name}"

        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith(f"{self.content.path.remote}"
                                   ) and treefile.endswith(".cs"):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set local path
        self.content.path.local = self.localpath

    async def update_repository(self):
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        await self.common_update()

        # Get appdaemon objects.
        if self.repository_manifest:
            if self.data.content_in_root:
                self.content.path.remote = ""

        if self.content.path.remote == "apps":
            self.data.domain = get_first_directory_in_directory(
                self.tree, self.content.path.remote)
            self.content.path.remote = f"apps/{self.data.name}"

        # Set local path
        self.content.path.local = self.localpath
Beispiel #24
0
class HacsPythonScript(HacsRepository):
    """python_scripts in HACS."""

    category = "python_script"

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.data.full_name = full_name
        self.data.category = "python_script"
        self.content.path.remote = "python_scripts"
        self.content.path.local = f"{self.hacs.system.config_path}/python_scripts"
        self.content.single = True
        self.logger = Logger(
            f"hacs.repository.{self.data.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        if self.data.content_in_root:
            self.content.path.remote = ""

        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith(f"{self.content.path.remote}"
                                   ) and treefile.endswith(".py"):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repository structure for {self.ref.replace('tags/','')} is not compliant"
            )

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set name
        find_file_name(self)

    async def update_repository(self):  # lgtm[py/similar-function]
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        # Run common update steps.
        await self.common_update()

        # Get python_script objects.
        if self.data.content_in_root:
            self.content.path.remote = ""

        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith(f"{self.content.path.remote}"
                                   ) and treefile.endswith(".py"):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repository structure for {self.ref.replace('tags/','')} is not compliant"
            )

        # Update name
        find_file_name(self)
Beispiel #25
0
class HacsTheme(HacsRepository):
    """Themes in HACS."""

    category = "theme"

    def __init__(self, full_name):
        """Initialize."""
        super().__init__()
        self.information.full_name = full_name
        self.information.category = self.category
        self.content.path.remote = "themes"
        self.content.path.local = f"{self.hacs.system.config_path}/themes"
        self.content.single = False
        self.logger = Logger(f"hacs.repository.{self.category}.{full_name}")

    async def validate_repository(self):
        """Validate."""
        # Run common validation steps.
        await self.common_validate()

        # Custom step 1: Validate content.
        compliant = False
        for treefile in self.treefiles:
            if treefile.startswith("themes/") and treefile.endswith(".yaml"):
                compliant = True
                break
        if not compliant:
            raise HacsException(
                f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
            )

        if self.data.content_in_root:
            self.content.path.remote = ""

        # Handle potential errors
        if self.validate.errors:
            for error in self.validate.errors:
                if not self.hacs.system.status.startup:
                    self.logger.error(error)
        return self.validate.success

    async def registration(self):
        """Registration."""
        if not await self.validate_repository():
            return False

        # Run common registration steps.
        await self.common_registration()

        # Set name
        if self.data.filename is not None:
            self.information.file_name = self.data.filename
        else:
            self.information.file_name = find_first_of_filetype(
                self.content.files, "yaml").split("/")[-1]
        self.information.name = self.information.file_name.replace(".yaml", "")
        self.content.path.local = (
            f"{self.hacs.system.config_path}/themes/{self.information.name}")

    async def update_repository(self):  # lgtm[py/similar-function]
        """Update."""
        if self.hacs.github.ratelimits.remaining == 0:
            return
        # Run common update steps.
        await self.common_update()

        # Get theme objects.
        if self.data.content_in_root:
            self.content.path.remote = ""

        # Update name
        if self.data.filename is not None:
            self.information.file_name = self.data.filename
        else:
            self.information.file_name = find_first_of_filetype(
                self.content.files, "yaml").split("/")[-1]
        self.information.name = self.information.file_name.replace(".yaml", "")
        self.content.path.local = (
            f"{self.hacs.system.config_path}/themes/{self.information.name}")
Beispiel #26
0
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
Beispiel #27
0
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