Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async wrapper for commonly used filesystem functions #3584

Merged
merged 9 commits into from
May 22, 2024
2 changes: 1 addition & 1 deletion custom_components/hacs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 7 additions & 8 deletions custom_components/hacs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
ludeeus marked this conversation as resolved.
Show resolved Hide resolved

async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for."""
Expand Down Expand Up @@ -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")
ludeeus marked this conversation as resolved.
Show resolved Hide resolved
):
return

Expand All @@ -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

Expand Down
7 changes: 3 additions & 4 deletions custom_components/hacs/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")):
Expand Down Expand Up @@ -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()
15 changes: 8 additions & 7 deletions custom_components/hacs/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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):
balloob marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
29 changes: 29 additions & 0 deletions custom_components/hacs/utils/file_system.py
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions tests/utils/test_fs_util.py
Original file line number Diff line number Diff line change
@@ -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")