diff --git a/custom_components/hacs/__init__.py b/custom_components/hacs/__init__.py index 3ddfd4ffdbf..52ecdd621f6 100644 --- a/custom_components/hacs/__init__.py +++ b/custom_components/hacs/__init__.py @@ -163,7 +163,7 @@ async def async_startup(): hacs.set_active_categories() async_register_websocket_commands(hass) - async_register_frontend(hass, hacs) + await async_register_frontend(hass, hacs) if hacs.configuration.config_type == ConfigurationType.YAML: hass.async_create_task( diff --git a/custom_components/hacs/base.py b/custom_components/hacs/base.py index 17524145b60..8ee9b2a81da 100644 --- a/custom_components/hacs/base.py +++ b/custom_components/hacs/base.py @@ -65,6 +65,7 @@ ) from .repositories import REPOSITORY_CLASSES from .utils.decode import decode_content +from .utils.file_system import async_exists from .utils.json import json_loads from .utils.logger import LOGGER from .utils.queue_manager import QueueManager @@ -474,7 +475,7 @@ def _write_file(): self.log.error("Could not write data to %s - %s", file_path, error) return False - return os.path.exists(file_path) + return await async_exists(self.hass, file_path) async def async_can_update(self) -> int: """Helper to calculate the number of repositories we can fetch data for.""" @@ -1180,11 +1181,10 @@ async def async_handle_critical_repositories(self, _=None) -> None: self.log.critical("Restarting Home Assistant") self.hass.async_create_task(self.hass.async_stop(100)) - @callback - def async_setup_frontend_endpoint_plugin(self) -> None: + async def async_setup_frontend_endpoint_plugin(self) -> None: """Setup the http endpoints for plugins if its not already handled.""" - if self.status.active_frontend_endpoint_plugin or not os.path.exists( - self.hass.config.path("www/community") + if self.status.active_frontend_endpoint_plugin or not await async_exists( + self.hass, self.hass.config.path("www/community") ): return @@ -1204,13 +1204,12 @@ def async_setup_frontend_endpoint_plugin(self) -> None: self.status.active_frontend_endpoint_plugin = True - @callback - def async_setup_frontend_endpoint_themes(self) -> None: + async def async_setup_frontend_endpoint_themes(self) -> None: """Setup the http endpoints for themes if its not already handled.""" if ( self.configuration.experimental or self.status.active_frontend_endpoint_theme - or not os.path.exists(self.hass.config.path("themes")) + or not await async_exists(self.hass, self.hass.config.path("themes")) ): return diff --git a/custom_components/hacs/frontend.py b/custom_components/hacs/frontend.py index c1db4928961..9e445c6178a 100644 --- a/custom_components/hacs/frontend.py +++ b/custom_components/hacs/frontend.py @@ -30,12 +30,11 @@ def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None: from .base import HacsBase -@callback -def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None: +async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None: """Register the frontend.""" # Setup themes endpoint if needed - hacs.async_setup_frontend_endpoint_themes() + await hacs.async_setup_frontend_endpoint_themes() # Register frontend if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")): @@ -84,4 +83,4 @@ def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None: ) # Setup plugin endpoint if needed - hacs.async_setup_frontend_endpoint_plugin() + await hacs.async_setup_frontend_endpoint_plugin() diff --git a/custom_components/hacs/repositories/base.py b/custom_components/hacs/repositories/base.py index 3ca020b701b..07cac8a24b4 100644 --- a/custom_components/hacs/repositories/base.py +++ b/custom_components/hacs/repositories/base.py @@ -31,6 +31,7 @@ from ..utils.backup import Backup, BackupNetDaemon from ..utils.decode import decode_content from ..utils.decorator import concurrent +from ..utils.file_system import async_exists, async_remove from ..utils.filters import filter_content_return_one_of_type from ..utils.json import json_loads from ..utils.logger import LOGGER @@ -796,8 +797,7 @@ async def remove_local_directory(self) -> None: f"{self.hacs.configuration.theme_path}/" f"{self.data.name}.yaml" ) - if os.path.exists(path): - os.remove(path) + await async_remove(self.hacs.hass, path, missing_ok=True) local_path = self.content.path.local elif self.data.category == "integration": if not self.data.domain: @@ -811,18 +811,18 @@ async def remove_local_directory(self) -> None: else: local_path = self.content.path.local - if os.path.exists(local_path): + if await async_exists(self.hacs.hass, local_path): if not is_safe(self.hacs, local_path): self.logger.error("%s Path %s is blocked from removal", self.string, local_path) return False self.logger.debug("%s Removing %s", self.string, local_path) if self.data.category in ["python_script", "template"]: - os.remove(local_path) + await async_remove(self.hacs.hass, local_path) else: shutil.rmtree(local_path) - while os.path.exists(local_path): + while await async_exists(self.hacs.hass, local_path): await sleep(1) else: self.logger.debug( @@ -942,8 +942,9 @@ async def async_install_repository(self, *, version: str | None = None, **_) -> await self.hacs.hass.async_add_executor_job(persistent_directory.create) elif self.repository_manifest.persistent_directory: - if os.path.exists( - f"{self.content.path.local}/{self.repository_manifest.persistent_directory}" + if await async_exists( + self.hacs.hass, + f"{self.content.path.local}/{self.repository_manifest.persistent_directory}", ): persistent_directory = Backup( hacs=self.hacs, diff --git a/custom_components/hacs/repositories/plugin.py b/custom_components/hacs/repositories/plugin.py index 34a2978b21b..d7f22839594 100644 --- a/custom_components/hacs/repositories/plugin.py +++ b/custom_components/hacs/repositories/plugin.py @@ -55,7 +55,7 @@ async def validate_repository(self): async def async_post_installation(self): """Run post installation steps.""" - self.hacs.async_setup_frontend_endpoint_plugin() + await self.hacs.async_setup_frontend_endpoint_plugin() @concurrent(concurrenttasks=10, backoff_time=5) async def update_repository(self, ignore_issues=False, force=False): diff --git a/custom_components/hacs/repositories/theme.py b/custom_components/hacs/repositories/theme.py index 41988f8eef8..3c0af1d7e97 100644 --- a/custom_components/hacs/repositories/theme.py +++ b/custom_components/hacs/repositories/theme.py @@ -37,7 +37,7 @@ async def async_post_installation(self): except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except pass - self.hacs.async_setup_frontend_endpoint_themes() + await self.hacs.async_setup_frontend_endpoint_themes() async def validate_repository(self): """Validate.""" diff --git a/custom_components/hacs/utils/file_system.py b/custom_components/hacs/utils/file_system.py new file mode 100644 index 00000000000..8c5fff929cc --- /dev/null +++ b/custom_components/hacs/utils/file_system.py @@ -0,0 +1,29 @@ +"""File system functions.""" + +from __future__ import annotations + +import os +from typing import TypeAlias + +from homeassistant.core import HomeAssistant + +# From typeshed +StrOrBytesPath: TypeAlias = str | bytes | os.PathLike[str] | os.PathLike[bytes] +FileDescriptorOrPath: TypeAlias = int | StrOrBytesPath + + +async def async_exists(hass: HomeAssistant, path: FileDescriptorOrPath) -> bool: + """Test whether a path exists.""" + return await hass.async_add_executor_job(os.path.exists, path) + + +async def async_remove( + hass: HomeAssistant, path: StrOrBytesPath, *, missing_ok: bool = False +) -> None: + """Remove a path.""" + try: + return await hass.async_add_executor_job(os.remove, path) + except FileNotFoundError: + if missing_ok: + return + raise diff --git a/tests/utils/test_fs_util.py b/tests/utils/test_fs_util.py new file mode 100644 index 00000000000..230799971ff --- /dev/null +++ b/tests/utils/test_fs_util.py @@ -0,0 +1,40 @@ +"""Test fs_util.""" + +from contextlib import nullcontext as does_not_raise +import os + +import pytest + +from custom_components.hacs.utils.file_system import async_exists, async_remove + +from tests.common import fixture + + +async def test_async_exists(hass, tmpdir): + """Test async_exists.""" + assert not await async_exists(hass, tmpdir / "tmptmp") + + open(tmpdir / "tmptmp", "w").close() + assert await async_exists(hass, tmpdir / "tmptmp") + + +async def test_async_remove(hass, tmpdir): + """Test async_remove.""" + assert not await async_exists(hass, tmpdir / "tmptmp") + with pytest.raises(FileNotFoundError): + await async_remove(hass, tmpdir / "tmptmp") + with pytest.raises(FileNotFoundError): + await async_remove(hass, tmpdir / "tmptmp", missing_ok=False) + await async_remove(hass, tmpdir / "tmptmp", missing_ok=True) + + open(tmpdir / "tmptmp", "w").close() + await async_remove(hass, tmpdir / "tmptmp") + assert not await async_exists(hass, tmpdir / "tmptmp") + + open(tmpdir / "tmptmp", "w").close() + await async_remove(hass, tmpdir / "tmptmp", missing_ok=False) + assert not await async_exists(hass, tmpdir / "tmptmp") + + open(tmpdir / "tmptmp", "w").close() + await async_remove(hass, tmpdir / "tmptmp", missing_ok=True) + assert not await async_exists(hass, tmpdir / "tmptmp")