Ejemplo n.º 1
0
def dummy_repository_base(repository=None):
    if repository is None:
        repository = HacsRepository()
    repository.hacs.hass = HomeAssistant()
    repository.hacs.hass.data = {"custom_components": []}
    repository.logger = Logger("hacs.test.test")
    repository.data.full_name = "test/test"
    repository.versions.available = "3"
    repository.status.selected_tag = "3"
    repository.ref = version_to_install(repository)
    repository.integration_manifest = {"config_flow": False, "domain": "test"}
    repository.releases.published_tags = ["1", "2", "3"]
    repository.data.update_data(repository_data)
    return repository
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    async def update_data(self):
        """Update data."""
        Logger("custom_components.healthchecksio").debug("Running update")
        # This is where the main logic to update platform data goes.
        try:
            verify_ssl = not self.self_hosted or self.site_root.startswith(
                "https")
            session = async_get_clientsession(self.hass, verify_ssl)
            headers = {"X-Api-Key": self.api_key}
            async with async_timeout.timeout(10):
                data = await session.get(f"{self.site_root}/api/v1/checks/",
                                         headers=headers)
                self.hass.data[DOMAIN_DATA]["data"] = await data.json()

                if self.self_hosted:
                    check_url = f"{self.site_root}/{self.ping_endpoint}/{self.check}"
                else:
                    check_url = f"https://hc-ping.com/{self.check}"
                await asyncio.sleep(1)  # needed for self-hosted instances
                await session.get(check_url)
        except Exception as error:  # pylint: disable=broad-except
            Logger("custom_components.healthchecksio").error(
                f"Could not update data - {error}")
Ejemplo n.º 4
0
    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
        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()
Ejemplo n.º 5
0
    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]
Ejemplo n.º 6
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
Ejemplo n.º 7
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)
Ejemplo n.º 8
0
async def check_files(hass):
    """Return bool that indicates if all files are present."""
    # Verify that the user downloaded all files.
    base = f"{hass.config.path()}/custom_components/{DOMAIN}/"
    missing = []
    for file in REQUIRED_FILES:
        fullpath = "{}{}".format(base, file)
        if not os.path.exists(fullpath):
            missing.append(file)

    if missing:
        Logger("custom_components.healthchecksio").critical(
            f"The following files are missing: {missing}")
        returnvalue = False
    else:
        returnvalue = True

    return returnvalue
Ejemplo n.º 9
0
    async def service_generate(call):
        """Generate the files."""
        base = hass.config.path()
        if ReadmeConfiguration.convert_lovelace:
            convert_lovelace(hass)
        custom_components = get_custom_components(hass)
        variables = {
            "custom_components": custom_components,
            "states": AllStates(hass)
        }

        with open(f"{base}/templates/README.j2", "r") as readme:
            content = readme.read()

        template = Template(content)
        try:
            render = template.render(variables)
            with open(f"{base}/README.md", "w") as out_file:
                out_file.write(render)
        except Exception as exception:
            Logger("custom_components.readme").error(exception)
class Backup:
    """Backup."""
    def __init__(self, local_path):
        """initialize."""
        self.logger = Logger("hacs.backup")
        self.local_path = local_path
        self.backup_path = "/tmp/hacs_backup"

    def create(self):
        """Create a backup in /tmp"""
        if os.path.exists(self.backup_path):
            rmtree(self.backup_path)
            while os.path.exists(self.backup_path):
                sleep(0.1)
        os.makedirs(self.backup_path, exist_ok=True)

        try:
            if os.path.isfile(self.local_path):
                copy2(self.local_path, self.backup_path)
                os.remove(self.local_path)
            else:
                copy_tree(self.local_path, self.backup_path)
                rmtree(self.local_path)
                while os.path.exists(self.local_path):
                    sleep(0.1)
            self.logger.debug(
                f"Backup for {self.local_path}, created in {self.backup_path}")
        except Exception:  # pylint: disable=broad-except
            pass

    def restore(self):
        """Restore from backup."""
        if os.path.isfile(self.local_path):
            os.remove(self.local_path)
        else:
            rmtree(self.local_path)
            while os.path.exists(self.local_path):
                sleep(0.1)
        copy2(self.backup_path, self.local_path)
        self.logger.debug(
            f"Restored {self.local_path}, from backup {self.backup_path}")

    def cleanup(self):
        """Cleanup backup files."""
        rmtree(self.backup_path)
        while os.path.exists(self.backup_path):
            sleep(0.1)
        self.logger.debug(f"Backup dir {self.backup_path} cleared")
Ejemplo n.º 11
0
def render_template(content, context):
    """Render templates in content."""
    # Fix None issues
    if context.releases.last_release_object is not None:
        prerelease = context.releases.last_release_object.prerelease
    else:
        prerelease = False

    # Render the template
    try:
        render = Template(content)
        render = render.render(
            installed=context.data.installed,
            pending_update=context.pending_upgrade,
            prerelease=prerelease,
            selected_tag=context.data.selected_tag,
            version_available=context.releases.last_release,
            version_installed=context.display_installed_version,
        )
        return render
    except Exception as exception:
        Logger("hacs.template").debug(exception)
        return content
Ejemplo n.º 12
0
async def async_setup_entry(hass, config_entry):
    """Set up this integration using UI."""
    # Print startup message
    Logger("custom_components.healthchecksio").info(
        CC_STARTUP_VERSION.format(name=DOMAIN,
                                  version=INTEGRATION_VERSION,
                                  issue_link=ISSUE_URL))

    # Check that all required files are present
    file_check = await check_files(hass)
    if not file_check:
        return False

    # Create DATA dict
    if DOMAIN_DATA not in hass.data:
        hass.data[DOMAIN_DATA] = {}
        if "data" not in hass.data[DOMAIN_DATA]:
            hass.data[DOMAIN_DATA] = {}

    # Get "global" configuration.
    api_key = config_entry.data.get("api_key")
    check = config_entry.data.get("check")
    self_hosted = config_entry.data.get("self_hosted")
    site_root = config_entry.data.get("site_root")
    ping_endpoint = config_entry.data.get("ping_endpoint")

    # Configure the client.
    hass.data[DOMAIN_DATA]["client"] = HealthchecksioData(
        hass, api_key, check, self_hosted, site_root, ping_endpoint)

    # Add binary_sensor
    hass.async_add_job(
        hass.config_entries.async_forward_entry_setup(config_entry,
                                                      "binary_sensor"))

    return True
Ejemplo n.º 13
0
class HacsAPI(HacsWebResponse):
    """HacsAPI class."""

    name = "hacsapi"

    def __init__(self):
        """Initialize."""
        self.logger = Logger("hacs.api")
        self.url = self.hacsapi + "/{endpoint}"

    async def post(self, request, endpoint):  # pylint: disable=unused-argument
        """Handle HACS API requests."""
        if self.system.disabled:
            return web.Response(status=404)
        self.endpoint = endpoint
        self.postdata = await request.post()
        self.raw_headers = request.raw_headers
        self.request = request
        self.logger.debug(f"Endpoint ({endpoint}) called")
        if self.configuration.dev:
            self.logger.debug(f"Raw headers ({self.raw_headers})")
            self.logger.debug(f"Postdata ({self.postdata})")
        if self.endpoint in APIRESPONSE:
            try:
                response = APIRESPONSE[self.endpoint]
                response = await response.response(self)
            except Exception as exception:
                render = self.render(f"error", message=exception)
                return web.Response(
                    body=render, content_type="text/html", charset="utf-8"
                )
        else:
            # Return default response.
            response = await APIRESPONSE["generic"].response(self)

        # set headers
        response.headers["Cache-Control"] = "max-age=0, must-revalidate"

        # serve the response
        return response
Ejemplo n.º 14
0
async def get_default_repos_orgs(github: type(AIOGitHub), 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:
        logger.error(f"{category} is not in {str(orgs.keys())}")
        return

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

    except AIOGitHubException as exception:
        logger.error(exception)

    return repositories
Ejemplo n.º 15
0
class Hacs:
    """The base class of HACS, nested thoughout the project."""

    token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}"
    hacsweb = f"/hacsweb/{token}"
    hacsapi = f"/hacsapi/{token}"
    repositories = []
    repo = None
    developer = Developer()
    data = None
    configuration = None
    logger = Logger("hacs")
    github = None
    hass = None
    version = None
    system = System()
    tasks = []
    common = HacsCommon()

    def get_by_id(self, repository_id):
        """Get repository by ID."""
        try:
            for repository in self.repositories:
                if repository.information.uid == repository_id:
                    return repository
        except Exception:  # pylint: disable=broad-except
            pass
        return None

    def get_by_name(self, repository_full_name):
        """Get repository by full_name."""
        try:
            for repository in self.repositories:
                if repository.information.full_name == repository_full_name:
                    return repository
        except Exception:  # pylint: disable=broad-except
            pass
        return None

    def is_known(self, repository_full_name):
        """Return a bool if the repository is known."""
        for repository in self.repositories:
            if repository.information.full_name == repository_full_name:
                return True
        return False

    @property
    def sorted_by_name(self):
        """Return a sorted(by name) list of repository objects."""
        return sorted(self.repositories, key=lambda x: x.display_name)

    @property
    def sorted_by_repository_name(self):
        """Return a sorted(by repository_name) list of repository objects."""
        return sorted(self.repositories, key=lambda x: x.information.full_name)

    async def register_repository(self, full_name, category, check=True):
        """Register a repository."""
        from ..repositories.repository import RERPOSITORY_CLASSES

        if full_name in self.common.skip:
            self.logger.debug(f"Skipping {full_name}")
            return

        if category not in RERPOSITORY_CLASSES:
            self.logger.error(f"{category} is not a valid repository category.")
            return False

        repository = RERPOSITORY_CLASSES[category](full_name)
        if check:
            try:
                await repository.registration()
                if repository.validate.errors:
                    self.common.skip.append(repository.information.full_name)
                    if not self.system.status.startup:
                        self.logger.error(f"Validation for {full_name} failed.")
                    return repository.validate.errors
                repository.logger.info("Registration complete")
            except AIOGitHubException as exception:
                self.logger.debug(self.github.ratelimits.remaining)
                self.logger.debug(self.github.ratelimits.reset_utc)
                self.common.skip.append(repository.information.full_name)
                if not self.system.status.startup:
                    self.logger.error(
                        f"Validation for {full_name} failed with {exception}."
                    )
                return
        self.hass.bus.fire(
            "hacs/repository",
            {
                "id": 1337,
                "action": "registration",
                "repository": repository.information.full_name,
            },
        )
        self.repositories.append(repository)

    async def startup_tasks(self):
        """Tasks tha are started after startup."""
        self.system.status.background_task = True
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        await self.load_known_repositories()
        self.clear_out_blacklisted_repositories()
        self.tasks.append(
            async_track_time_interval(
                self.hass, self.recuring_tasks_installed, timedelta(minutes=30)
            )
        )
        self.tasks.append(
            async_track_time_interval(
                self.hass, self.recuring_tasks_all, timedelta(minutes=800)
            )
        )

        self.system.status.startup = False
        self.system.status.background_task = False
        self.data.write()

    async def recuring_tasks_installed(self, notarealarg=None):
        """Recuring tasks for installed repositories."""
        self.logger.debug(
            "Starting recuring background task for installed repositories"
        )
        self.system.status.background_task = True
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        for repository in self.repositories:
            if repository.status.installed:
                try:
                    await repository.update_repository()
                    repository.logger.debug("Information update done.")
                except AIOGitHubException:
                    self.system.status.background_task = False
                    self.data.write()
                    self.logger.debug(
                        "Recuring background task for installed repositories done"
                    )
                    return
        self.system.status.background_task = False
        self.data.write()
        self.logger.debug("Recuring background task for installed repositories done")

    async def recuring_tasks_all(self, notarealarg=None):
        """Recuring tasks for all repositories."""
        self.logger.debug("Starting recuring background task for all repositories")
        self.system.status.background_task = True
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        for repository in self.repositories:
            try:
                await repository.update_repository()
                repository.logger.debug("Information update done.")
            except AIOGitHubException:
                self.system.status.background_task = False
                self.data.write()
                self.logger.debug("Recuring background task for all repositories done")
                return
        await self.load_known_repositories()
        self.clear_out_blacklisted_repositories()
        self.system.status.background_task = False
        self.data.write()
        self.hass.bus.fire("hacs/repository", {"action": "reload"})
        self.logger.debug("Recuring background task for all repositories done")

    def clear_out_blacklisted_repositories(self):
        """Clear out blaclisted repositories."""
        need_to_save = False
        for repository in self.common.blacklist:
            if self.is_known(repository):
                repository = self.get_by_name(repository)
                if repository.status.installed:
                    self.logger.error(
                        f"You have {repository.information.full_name} installed with HACS, this repositroy have not been blacklisted, please consider removing it."
                    )
                else:
                    need_to_save = True
                    repository.remove()

        if need_to_save:
            self.data.write()

    async def get_repositories(self):
        """Return a list of repositories."""
        repositories = {}
        if self.configuration.dev:
            if self.developer.devcontainer:
                repositories = {
                    "appdaemon": ["ludeeus/ad-hacs"],
                    "integration": ["ludeeus/integration-hacs"],
                    "plugin": ["maykar/compact-custom-header"],
                    "python_script": ["ludeeus/ps-hacs"],
                    "theme": ["ludeeus/theme-hacs"],
                }
        else:
            for category in self.common.categories:
                remote = await self.repo.get_contents(
                    f"repositories/{category}", "data"
                )
                repositories[category] = json.loads(remote.content)
                if category == "plugin":
                    org = await self.github.get_org_repos("custom-cards")
                    for repo in org:
                        repositories[category].append(repo.full_name)
                if category == "integration":
                    org = await self.github.get_org_repos("custom-components")
                    for repo in org:
                        repositories[category].append(repo.full_name)

        for category in repositories:
            for repo in repositories[category]:
                if repo not in self.common.default:
                    self.common.default.append(repo)
        return repositories

    async def load_known_repositories(self):
        """Load known repositories."""
        self.logger.info("Loading known repositories")
        blacklist = await self.repo.get_contents("repositories/blacklist", "data")
        repositories = await self.get_repositories()

        for item in json.loads(blacklist.content):
            if item not in self.common.blacklist:
                self.common.blacklist.append(item)

        for category in repositories:
            for repo in repositories[category]:
                if repo in self.common.blacklist:
                    continue
                if self.is_known(repo):
                    continue
                try:
                    await self.register_repository(repo, category)
                except (AIOGitHubException, AIOGitHubRatelimit):
                    pass
 def __init__(self, local_path):
     """initialize."""
     self.logger = Logger("hacs.backup")
     self.local_path = local_path
     self.backup_path = "/tmp/hacs_backup"
Ejemplo n.º 17
0
 def __init__(self):
     """Initialize."""
     self.logger = Logger("hacs.api")
     self.url = self.hacsapi + "/{endpoint}"
Ejemplo n.º 18
0
class Hacs:
    """The base class of HACS, nested thoughout the project."""

    token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}"
    hacsweb = f"/hacsweb/{token}"
    hacsapi = f"/hacsapi/{token}"
    repositories = []
    frontend = HacsFrontend()
    repo = None
    data_repo = None
    developer = Developer()
    data = None
    configuration = None
    logger = Logger("hacs")
    github = None
    hass = None
    version = None
    factory = HacsTaskFactory()
    system = System()
    recuring_tasks = []
    common = HacsCommon()

    @staticmethod
    def init(hass, github_token):
        """Return a initialized HACS object."""
        return Hacs()

    def get_by_id(self, repository_id):
        """Get repository by ID."""
        try:
            for repository in self.repositories:
                if repository.information.uid == repository_id:
                    return repository
        except Exception:  # pylint: disable=broad-except
            pass
        return None

    def get_by_name(self, repository_full_name):
        """Get repository by full_name."""
        try:
            for repository in self.repositories:
                if repository.information.full_name == repository_full_name:
                    return repository
        except Exception:  # pylint: disable=broad-except
            pass
        return None

    def is_known(self, repository_full_name):
        """Return a bool if the repository is known."""
        for repository in self.repositories:
            if repository.information.full_name == repository_full_name:
                return True
        return False

    @property
    def sorted_by_name(self):
        """Return a sorted(by name) list of repository objects."""
        return sorted(self.repositories, key=lambda x: x.display_name)

    @property
    def sorted_by_repository_name(self):
        """Return a sorted(by repository_name) list of repository objects."""
        return sorted(self.repositories, key=lambda x: x.information.full_name)

    async def register_repository(self, full_name, category, check=True):
        """Register a repository."""
        from ..repositories.repository import RERPOSITORY_CLASSES

        if full_name in self.common.skip:
            if full_name != "hacs/integration":
                self.logger.debug(f"Skipping {full_name}")
                return

        if category not in RERPOSITORY_CLASSES:
            self.logger.error(f"{category} is not a valid repository category.")
            return False

        repository = RERPOSITORY_CLASSES[category](full_name)
        if check:
            try:
                await repository.registration()
                if self.system.status.new:
                    repository.status.new = False
                if repository.validate.errors:
                    self.common.skip.append(repository.information.full_name)
                    if not self.system.status.startup:
                        self.logger.error(f"Validation for {full_name} failed.")
                    return repository.validate.errors
                repository.logger.info("Registration complete")
            except AIOGitHubException as exception:
                self.logger.debug(self.github.ratelimits.remaining)
                self.logger.debug(self.github.ratelimits.reset_utc)
                self.common.skip.append(repository.information.full_name)
                # if not self.system.status.startup:
                if self.system.status.startup:
                    self.logger.error(
                        f"Validation for {full_name} failed with {exception}."
                    )
                return exception
        self.hass.bus.async_fire(
            "hacs/repository",
            {
                "id": 1337,
                "action": "registration",
                "repository": repository.information.full_name,
                "repository_id": repository.information.uid,
            },
        )
        self.repositories.append(repository)

    async def startup_tasks(self):
        """Tasks tha are started after startup."""
        self.system.status.background_task = True
        self.hass.bus.async_fire("hacs/status", {})
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)

        await self.handle_critical_repositories_startup()
        await self.handle_critical_repositories()
        await self.load_known_repositories()
        await self.clear_out_blacklisted_repositories()

        self.recuring_tasks.append(
            async_track_time_interval(
                self.hass, self.recuring_tasks_installed, timedelta(minutes=30)
            )
        )
        self.recuring_tasks.append(
            async_track_time_interval(
                self.hass, self.recuring_tasks_all, timedelta(minutes=800)
            )
        )

        self.hass.bus.async_fire("hacs/reload", {"force": True})
        await self.recuring_tasks_installed()

        self.system.status.startup = False
        self.system.status.new = False
        self.system.status.background_task = False
        self.hass.bus.async_fire("hacs/status", {})
        await self.data.async_write()

    async def handle_critical_repositories_startup(self):
        """Handled critical repositories during startup."""
        alert = False
        critical = await async_load_from_store(self.hass, "critical")
        if not critical:
            return
        for repo in critical:
            if not repo["acknowledged"]:
                alert = True
        if alert:
            self.logger.critical("URGENT!: Check the HACS panel!")
            self.hass.components.persistent_notification.create(
                title="URGENT!", message="**Check the HACS panel!**"
            )

    async def handle_critical_repositories(self):
        """Handled critical repositories during runtime."""
        # Get critical repositories
        instored = []
        critical = []
        was_installed = False

        try:
            critical = await self.data_repo.get_contents("critical")
            critical = json.loads(critical.content)
        except AIOGitHubException:
            pass

        if not critical:
            self.logger.debug("No critical repositories")
            return

        stored_critical = await async_load_from_store(self.hass, "critical")

        for stored in stored_critical or []:
            instored.append(stored["repository"])

        stored_critical = []

        for repository in critical:
            self.common.blacklist.append(repository["repository"])
            repo = self.get_by_name(repository["repository"])

            stored = {
                "repository": repository["repository"],
                "reason": repository["reason"],
                "link": repository["link"],
                "acknowledged": True,
            }

            if repository["repository"] not in instored:
                if repo is not None and repo.installed:
                    self.logger.critical(
                        f"Removing repository {repository['repository']}, it is marked as critical"
                    )
                    was_installed = True
                    stored["acknowledged"] = False
                    # Uninstall from HACS
                    repo.remove()
                    await repo.uninstall()
            stored_critical.append(stored)

        # Save to FS
        await async_save_to_store(self.hass, "critical", stored_critical)

        # Resart HASS
        if was_installed:
            self.logger.critical("Resarting Home Assistant")
            self.hass.async_create_task(self.hass.async_stop(100))

    async def recuring_tasks_installed(self, notarealarg=None):
        """Recuring tasks for installed repositories."""
        self.logger.debug(
            "Starting recuring background task for installed repositories"
        )
        self.system.status.background_task = True
        self.hass.bus.async_fire("hacs/status", {})
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        for repository in self.repositories:
            if (
                repository.status.installed
                and repository.category in self.common.categories
            ):
                self.factory.tasks.append(self.factory.safe_update(repository))

        await self.factory.execute()
        await self.handle_critical_repositories()
        self.system.status.background_task = False
        self.hass.bus.async_fire("hacs/status", {})
        await self.data.async_write()
        self.logger.debug("Recuring background task for installed repositories done")

    async def recuring_tasks_all(self, notarealarg=None):
        """Recuring tasks for all repositories."""
        self.logger.debug("Starting recuring background task for all repositories")
        self.system.status.background_task = True
        self.hass.bus.async_fire("hacs/status", {})
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        for repository in self.repositories:
            if repository.category in self.common.categories:
                self.factory.tasks.append(self.factory.safe_common_update(repository))

        await self.factory.execute()
        await self.load_known_repositories()
        await self.clear_out_blacklisted_repositories()
        self.system.status.background_task = False
        await self.data.async_write()
        self.hass.bus.async_fire("hacs/status", {})
        self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
        self.logger.debug("Recuring background task for all repositories done")

    async def clear_out_blacklisted_repositories(self):
        """Clear out blaclisted repositories."""
        need_to_save = False
        for repository in self.common.blacklist:
            if self.is_known(repository):
                repository = self.get_by_name(repository)
                if repository.status.installed:
                    self.logger.warning(
                        f"You have {repository.information.full_name} installed with HACS "
                        + "this repository has been blacklisted, please consider removing it."
                    )
                else:
                    need_to_save = True
                    repository.remove()

        if need_to_save:
            await self.data.async_write()

    async def get_repositories(self):
        """Return a list of repositories."""
        repositories = {}
        for category in self.common.categories:
            repositories[category] = await get_default_repos_lists(
                self.github, category
            )
            org = await get_default_repos_orgs(self.github, category)
            for repo in org:
                repositories[category].append(repo)

        for category in repositories:
            for repo in repositories[category]:
                if repo not in self.common.default:
                    self.common.default.append(repo)
        return repositories

    async def load_known_repositories(self):
        """Load known repositories."""
        self.logger.info("Loading known repositories")
        repositories = await self.get_repositories()

        for item in await get_default_repos_lists(self.github, "blacklist"):
            if item not in self.common.blacklist:
                self.common.blacklist.append(item)

        for category in repositories:
            for repo in repositories[category]:
                if repo in self.common.blacklist:
                    continue
                if self.is_known(repo):
                    continue
                self.factory.tasks.append(
                    self.factory.safe_register(self, repo, category)
                )
        await self.factory.execute()
Ejemplo n.º 19
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
Ejemplo n.º 20
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

    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
Ejemplo n.º 21
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
Ejemplo n.º 22
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
Ejemplo n.º 23
0
class Hacs:
    """The base class of HACS, nested thoughout the project."""

    token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}"
    hacsweb = f"/hacsweb/{token}"
    hacsapi = f"/hacsapi/{token}"
    repositories = []
    frontend = HacsFrontend()
    repo = None
    data_repo = None
    developer = Developer()
    data = None
    configuration = None
    logger = Logger("hacs")
    github = None
    hass = None
    version = None
    session = None
    factory = HacsTaskFactory()
    queue = QueueManager()
    system = System()
    recuring_tasks = []
    common = HacsCommon()

    @staticmethod
    def init(hass, github_token):
        """Return a initialized HACS object."""
        return Hacs()

    def get_by_id(self, repository_id):
        """Get repository by ID."""
        try:
            for repository in self.repositories:
                if repository.information.uid == repository_id:
                    return repository
        except Exception:  # pylint: disable=broad-except
            pass
        return None

    def get_by_name(self, repository_full_name):
        """Get repository by full_name."""
        try:
            for repository in self.repositories:
                if repository.data.full_name.lower() == repository_full_name.lower():
                    return repository
        except Exception:  # pylint: disable=broad-except
            pass
        return None

    def is_known(self, repository_full_name):
        """Return a bool if the repository is known."""
        return repository_full_name.lower() in [
            x.data.full_name.lower() for x in self.repositories
        ]

    @property
    def sorted_by_name(self):
        """Return a sorted(by name) list of repository objects."""
        return sorted(self.repositories, key=lambda x: x.display_name)

    @property
    def sorted_by_repository_name(self):
        """Return a sorted(by repository_name) list of repository objects."""
        return sorted(self.repositories, key=lambda x: x.data.full_name)

    async def register_repository(self, full_name, category, check=True):
        """Register a repository."""
        await register_repository(full_name, category, check=True)

    async def startup_tasks(self):
        """Tasks tha are started after startup."""
        self.system.status.background_task = True
        await self.hass.async_add_executor_job(setup_extra_stores)
        self.hass.bus.async_fire("hacs/status", {})
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)

        await self.handle_critical_repositories_startup()
        await self.handle_critical_repositories()
        await self.load_known_repositories()
        await self.clear_out_removed_repositories()

        self.recuring_tasks.append(
            async_track_time_interval(
                self.hass, self.recuring_tasks_installed, timedelta(minutes=30)
            )
        )
        self.recuring_tasks.append(
            async_track_time_interval(
                self.hass, self.recuring_tasks_all, timedelta(minutes=800)
            )
        )
        self.recuring_tasks.append(
            async_track_time_interval(
                self.hass, self.prosess_queue, timedelta(minutes=10)
            )
        )

        self.hass.bus.async_fire("hacs/reload", {"force": True})
        await self.recuring_tasks_installed()

        await self.prosess_queue()

        self.system.status.startup = False
        self.system.status.new = False
        self.system.status.background_task = False
        self.hass.bus.async_fire("hacs/status", {})
        await self.data.async_write()

    async def handle_critical_repositories_startup(self):
        """Handled critical repositories during startup."""
        alert = False
        critical = await async_load_from_store(self.hass, "critical")
        if not critical:
            return
        for repo in critical:
            if not repo["acknowledged"]:
                alert = True
        if alert:
            self.logger.critical("URGENT!: Check the HACS panel!")
            self.hass.components.persistent_notification.create(
                title="URGENT!", message="**Check the HACS panel!**"
            )

    async def handle_critical_repositories(self):
        """Handled critical repositories during runtime."""
        # Get critical repositories
        instored = []
        critical = []
        was_installed = False

        try:
            critical = await self.data_repo.get_contents("critical")
            critical = json.loads(critical.content)
        except AIOGitHubException:
            pass

        if not critical:
            self.logger.debug("No critical repositories")
            return

        stored_critical = await async_load_from_store(self.hass, "critical")

        for stored in stored_critical or []:
            instored.append(stored["repository"])

        stored_critical = []

        for repository in critical:
            removed_repo = get_removed(repository["repository"])
            removed_repo.removal_type = "critical"
            repo = self.get_by_name(repository["repository"])

            stored = {
                "repository": repository["repository"],
                "reason": repository["reason"],
                "link": repository["link"],
                "acknowledged": True,
            }
            if repository["repository"] not in instored:
                if repo is not None and repo.installed:
                    self.logger.critical(
                        f"Removing repository {repository['repository']}, it is marked as critical"
                    )
                    was_installed = True
                    stored["acknowledged"] = False
                    # Uninstall from HACS
                    repo.remove()
                    await repo.uninstall()
            stored_critical.append(stored)
            removed_repo.update_data(stored)

        # Save to FS
        await async_save_to_store(self.hass, "critical", stored_critical)

        # Resart HASS
        if was_installed:
            self.logger.critical("Resarting Home Assistant")
            self.hass.async_create_task(self.hass.async_stop(100))

    async def prosess_queue(self, notarealarg=None):
        """Recuring tasks for installed repositories."""
        if not self.queue.has_pending_tasks:
            self.logger.debug("Nothing in the queue")
            return
        if self.queue.running:
            self.logger.debug("Queue is already running")
            return

        can_update = await get_fetch_updates_for(self.github)
        if can_update == 0:
            self.logger.info(
                "HACS is ratelimited, repository updates will resume later."
            )
        else:
            self.system.status.background_task = True
            self.hass.bus.async_fire("hacs/status", {})
            await self.queue.execute(can_update)
            self.system.status.background_task = False
            self.hass.bus.async_fire("hacs/status", {})

    async def recuring_tasks_installed(self, notarealarg=None):
        """Recuring tasks for installed repositories."""
        self.logger.debug(
            "Starting recuring background task for installed repositories"
        )
        self.system.status.background_task = True
        self.hass.bus.async_fire("hacs/status", {})
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        for repository in self.repositories:
            if (
                repository.status.installed
                and repository.data.category in self.common.categories
            ):
                self.queue.add(self.factory.safe_update(repository))

        await self.handle_critical_repositories()
        self.system.status.background_task = False
        self.hass.bus.async_fire("hacs/status", {})
        await self.data.async_write()
        self.logger.debug("Recuring background task for installed repositories done")

    async def recuring_tasks_all(self, notarealarg=None):
        """Recuring tasks for all repositories."""
        self.logger.debug("Starting recuring background task for all repositories")
        await self.hass.async_add_executor_job(setup_extra_stores)
        self.system.status.background_task = True
        self.hass.bus.async_fire("hacs/status", {})
        self.logger.debug(self.github.ratelimits.remaining)
        self.logger.debug(self.github.ratelimits.reset_utc)
        for repository in self.repositories:
            if repository.data.category in self.common.categories:
                self.queue.add(self.factory.safe_common_update(repository))

        await self.load_known_repositories()
        await self.clear_out_removed_repositories()
        self.system.status.background_task = False
        await self.data.async_write()
        self.hass.bus.async_fire("hacs/status", {})
        self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
        self.logger.debug("Recuring background task for all repositories done")

    async def clear_out_removed_repositories(self):
        """Clear out blaclisted repositories."""
        need_to_save = False
        for removed in removed_repositories:
            if self.is_known(removed.repository):
                repository = self.get_by_name(removed.repository)
                if repository.status.installed and removed.removal_type != "critical":
                    self.logger.warning(
                        f"You have {repository.data.full_name} installed with HACS "
                        + f"this repository has been removed, please consider removing it. "
                        + f"Removal reason ({removed.removal_type})"
                    )
                else:
                    need_to_save = True
                    repository.remove()

        if need_to_save:
            await self.data.async_write()

    async def get_repositories(self):
        """Return a list of repositories."""
        repositories = {}
        for category in self.common.categories:
            repositories[category] = await get_default_repos_lists(
                self.session, self.configuration.token, category
            )
            org = await get_default_repos_orgs(self.github, category)
            for repo in org:
                repositories[category].append(repo)

        for category in repositories:
            for repo in repositories[category]:
                if repo not in self.common.default:
                    self.common.default.append(repo)
        return repositories

    async def load_known_repositories(self):
        """Load known repositories."""
        self.logger.info("Loading known repositories")
        repositories = await self.get_repositories()

        for item in await get_default_repos_lists(
            self.session, self.configuration.token, "removed"
        ):
            removed = get_removed(item["repository"])
            removed.reason = item.get("reason")
            removed.link = item.get("link")
            removed.removal_type = item.get("removal_type")

        for category in repositories:
            for repo in repositories[category]:
                if is_removed(repo):
                    continue
                if self.is_known(repo):
                    continue
                self.queue.add(self.factory.safe_register(repo, category))
Ejemplo n.º 24
0
 def __init__(self, local_path, backup_path=BACKUP_PATH):
     """initialize."""
     self.logger = Logger("hacs.backup")
     self.local_path = local_path
     self.backup_path = backup_path
     self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}"
Ejemplo n.º 25
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
Ejemplo n.º 26
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)
Ejemplo n.º 27
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)
Ejemplo n.º 28
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
Ejemplo n.º 29
0
 def __init__(self):
     """Initialize."""
     self.logger = Logger("hacs.data")
Ejemplo n.º 30
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', '')}"