mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Remove WebDAV properties and rely on metadata file (#140539)
This commit is contained in:
parent
d952e8186f
commit
99b140f73f
@ -5,10 +5,10 @@ from __future__ import annotations
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
from functools import wraps
|
||||
import logging
|
||||
from time import time
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from aiowebdav2 import Property, PropertyRequest
|
||||
from aiowebdav2.exceptions import UnauthorizedError, WebDavError
|
||||
from propcache.api import cached_property
|
||||
|
||||
@ -28,9 +28,8 @@ from .const import CONF_BACKUP_PATH, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
METADATA_VERSION = "1"
|
||||
BACKUP_TIMEOUT = ClientTimeout(connect=10, total=43200)
|
||||
NAMESPACE = "https://home-assistant.io"
|
||||
CACHE_TTL = 300
|
||||
|
||||
|
||||
async def async_get_backup_agents(
|
||||
@ -96,23 +95,6 @@ def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
return f"{base_name}.tar", f"{base_name}.metadata.json"
|
||||
|
||||
|
||||
def _is_current_metadata_version(properties: list[Property]) -> bool:
|
||||
"""Check if any property is of the current metadata version."""
|
||||
return any(
|
||||
prop.value == METADATA_VERSION
|
||||
for prop in properties
|
||||
if prop.namespace == NAMESPACE and prop.name == "metadata_version"
|
||||
)
|
||||
|
||||
|
||||
def _backup_id_from_properties(properties: list[Property]) -> str | None:
|
||||
"""Return the backup ID from properties."""
|
||||
for prop in properties:
|
||||
if prop.namespace == NAMESPACE and prop.name == "backup_id":
|
||||
return prop.value
|
||||
return None
|
||||
|
||||
|
||||
class WebDavBackupAgent(BackupAgent):
|
||||
"""Backup agent interface."""
|
||||
|
||||
@ -126,6 +108,8 @@ class WebDavBackupAgent(BackupAgent):
|
||||
self._client = entry.runtime_data
|
||||
self.name = entry.title
|
||||
self.unique_id = entry.entry_id
|
||||
self._cache_metadata_files: dict[str, AgentBackup] = {}
|
||||
self._cache_expiration = time()
|
||||
|
||||
@cached_property
|
||||
def _backup_path(self) -> str:
|
||||
@ -182,27 +166,14 @@ class WebDavBackupAgent(BackupAgent):
|
||||
f"{self._backup_path}/{filename_meta}",
|
||||
)
|
||||
|
||||
await self._client.set_property_batch(
|
||||
f"{self._backup_path}/{filename_meta}",
|
||||
[
|
||||
Property(
|
||||
namespace=NAMESPACE,
|
||||
name="backup_id",
|
||||
value=backup.backup_id,
|
||||
),
|
||||
Property(
|
||||
namespace=NAMESPACE,
|
||||
name="metadata_version",
|
||||
value=METADATA_VERSION,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Uploaded metadata file for %s",
|
||||
f"{self._backup_path}/{filename_meta}",
|
||||
)
|
||||
|
||||
# reset cache
|
||||
self._cache_expiration = time()
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_delete_backup(
|
||||
self,
|
||||
@ -226,14 +197,13 @@ class WebDavBackupAgent(BackupAgent):
|
||||
backup_path,
|
||||
)
|
||||
|
||||
# reset cache
|
||||
self._cache_expiration = time()
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||
"""List backups."""
|
||||
metadata_files = await self._list_metadata_files()
|
||||
return [
|
||||
await self._download_metadata(metadata_file)
|
||||
for metadata_file in metadata_files.values()
|
||||
]
|
||||
return list((await self._list_cached_metadata_files()).values())
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_get_backup(
|
||||
@ -244,38 +214,35 @@ class WebDavBackupAgent(BackupAgent):
|
||||
"""Return a backup."""
|
||||
return await self._find_backup_by_id(backup_id)
|
||||
|
||||
async def _list_metadata_files(self) -> dict[str, str]:
|
||||
"""List metadata files."""
|
||||
files = await self._client.list_with_properties(
|
||||
self._backup_path,
|
||||
[
|
||||
PropertyRequest(
|
||||
namespace=NAMESPACE,
|
||||
name="metadata_version",
|
||||
),
|
||||
PropertyRequest(
|
||||
namespace=NAMESPACE,
|
||||
name="backup_id",
|
||||
),
|
||||
],
|
||||
)
|
||||
return {
|
||||
backup_id: file_name
|
||||
for file_name, properties in files.items()
|
||||
if file_name.endswith(".json") and _is_current_metadata_version(properties)
|
||||
if (backup_id := _backup_id_from_properties(properties))
|
||||
}
|
||||
async def _list_cached_metadata_files(self) -> dict[str, AgentBackup]:
|
||||
"""List metadata files with a cache."""
|
||||
if time() <= self._cache_expiration:
|
||||
return self._cache_metadata_files
|
||||
|
||||
async def _download_metadata(path: str) -> AgentBackup:
|
||||
"""Download metadata file."""
|
||||
iterator = await self._client.download_iter(path)
|
||||
metadata = await anext(iterator)
|
||||
return AgentBackup.from_dict(json_loads_object(metadata))
|
||||
|
||||
async def _list_metadata_files() -> dict[str, AgentBackup]:
|
||||
"""List metadata files."""
|
||||
files = await self._client.list_files(self._backup_path)
|
||||
return {
|
||||
metadata_content.backup_id: metadata_content
|
||||
for file_name in files
|
||||
if file_name.endswith(".json")
|
||||
if (metadata_content := await _download_metadata(file_name))
|
||||
}
|
||||
|
||||
self._cache_metadata_files = await _list_metadata_files()
|
||||
self._cache_expiration = time() + CACHE_TTL
|
||||
return self._cache_metadata_files
|
||||
|
||||
async def _find_backup_by_id(self, backup_id: str) -> AgentBackup:
|
||||
"""Find a backup by its backup ID on remote."""
|
||||
metadata_files = await self._list_metadata_files()
|
||||
metadata_files = await self._list_cached_metadata_files()
|
||||
if metadata_file := metadata_files.get(backup_id):
|
||||
return await self._download_metadata(metadata_file)
|
||||
return metadata_file
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
async def _download_metadata(self, path: str) -> AgentBackup:
|
||||
"""Download metadata file."""
|
||||
iterator = await self._client.download_iter(path)
|
||||
metadata = await anext(iterator)
|
||||
return AgentBackup.from_dict(json_loads_object(metadata))
|
||||
|
@ -9,7 +9,7 @@ import pytest
|
||||
from homeassistant.components.webdav.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||
|
||||
from .const import BACKUP_METADATA, MOCK_LIST_WITH_PROPERTIES
|
||||
from .const import BACKUP_METADATA, MOCK_LIST_FILES
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -58,7 +58,7 @@ def mock_webdav_client() -> Generator[AsyncMock]:
|
||||
mock = mock_webdav_client.return_value
|
||||
mock.check.return_value = True
|
||||
mock.mkdir.return_value = True
|
||||
mock.list_with_properties.return_value = MOCK_LIST_WITH_PROPERTIES
|
||||
mock.list_files.return_value = MOCK_LIST_FILES
|
||||
mock.download_iter.side_effect = _download_mock
|
||||
mock.upload_iter.return_value = None
|
||||
mock.clean.return_value = None
|
||||
|
@ -1,7 +1,5 @@
|
||||
"""Constants for WebDAV tests."""
|
||||
|
||||
from aiowebdav2 import Property
|
||||
|
||||
BACKUP_METADATA = {
|
||||
"addons": [],
|
||||
"backup_id": "23e64aec",
|
||||
@ -16,18 +14,7 @@ BACKUP_METADATA = {
|
||||
"size": 34519040,
|
||||
}
|
||||
|
||||
MOCK_LIST_WITH_PROPERTIES = {
|
||||
"/Automatic_backup_2025.2.1_2025-02-10_18.31_30202686.tar": [],
|
||||
"/Automatic_backup_2025.2.1_2025-02-10_18.31_30202686.metadata.json": [
|
||||
Property(
|
||||
namespace="https://home-assistant.io",
|
||||
name="backup_id",
|
||||
value="23e64aec",
|
||||
),
|
||||
Property(
|
||||
namespace="https://home-assistant.io",
|
||||
name="metadata_version",
|
||||
value="1",
|
||||
),
|
||||
],
|
||||
}
|
||||
MOCK_LIST_FILES = [
|
||||
"/Automatic_backup_2025.2.1_2025-02-10_18.31_30202686.tar",
|
||||
"/Automatic_backup_2025.2.1_2025-02-10_18.31_30202686.metadata.json",
|
||||
]
|
||||
|
@ -6,7 +6,6 @@ from collections.abc import AsyncGenerator
|
||||
from io import StringIO
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiowebdav2 import Property
|
||||
from aiowebdav2.exceptions import UnauthorizedError, WebDavError
|
||||
import pytest
|
||||
|
||||
@ -17,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.backup import async_initialize_backup
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import BACKUP_METADATA, MOCK_LIST_WITH_PROPERTIES
|
||||
from .const import BACKUP_METADATA
|
||||
|
||||
from tests.common import AsyncMock, MockConfigEntry
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
@ -184,7 +183,6 @@ async def test_agents_upload(
|
||||
|
||||
assert resp.status == 201
|
||||
assert webdav_client.upload_iter.call_count == 2
|
||||
assert webdav_client.set_property_batch.call_count == 1
|
||||
|
||||
|
||||
async def test_agents_download(
|
||||
@ -211,7 +209,7 @@ async def test_error_on_agents_download(
|
||||
"""Test we get not found on a not existing backup on download."""
|
||||
client = await hass_client()
|
||||
backup_id = BACKUP_METADATA["backup_id"]
|
||||
webdav_client.list_with_properties.side_effect = [MOCK_LIST_WITH_PROPERTIES, {}]
|
||||
webdav_client.list_files.return_value = []
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/backup/download/{backup_id}?agent_id={DOMAIN}.{mock_config_entry.entry_id}"
|
||||
@ -262,7 +260,7 @@ async def test_agents_delete_not_found_does_not_throw(
|
||||
webdav_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test agent delete backup."""
|
||||
webdav_client.list_with_properties.return_value = {}
|
||||
webdav_client.list_files.return_value = {}
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
@ -283,7 +281,7 @@ async def test_agents_backup_not_found(
|
||||
webdav_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test backup not found."""
|
||||
webdav_client.list_with_properties.return_value = []
|
||||
webdav_client.list_files.return_value = []
|
||||
backup_id = BACKUP_METADATA["backup_id"]
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id})
|
||||
@ -300,7 +298,7 @@ async def test_raises_on_403(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we raise on 403."""
|
||||
webdav_client.list_with_properties.side_effect = UnauthorizedError(
|
||||
webdav_client.list_files.side_effect = UnauthorizedError(
|
||||
"https://webdav.example.com"
|
||||
)
|
||||
backup_id = BACKUP_METADATA["backup_id"]
|
||||
@ -324,30 +322,3 @@ async def test_listeners_get_cleaned_up(hass: HomeAssistant) -> None:
|
||||
remove_listener()
|
||||
|
||||
assert hass.data.get(DATA_BACKUP_AGENT_LISTENERS) is None
|
||||
|
||||
|
||||
async def test_metadata_misses_backup_id(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
webdav_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test getting a backup when metadata has backup id property."""
|
||||
MOCK_LIST_WITH_PROPERTIES[
|
||||
"/Automatic_backup_2025.2.1_2025-02-10_18.31_30202686.metadata.json"
|
||||
] = [
|
||||
Property(
|
||||
namespace="homeassistant",
|
||||
name="metadata_version",
|
||||
value="1",
|
||||
)
|
||||
]
|
||||
webdav_client.list_with_properties.return_value = MOCK_LIST_WITH_PROPERTIES
|
||||
|
||||
backup_id = BACKUP_METADATA["backup_id"]
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id})
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"]["backup"] is None
|
||||
|
Loading…
x
Reference in New Issue
Block a user