mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Correct backup filename on delete or download of cloud backup (#138704)
* Correct backup filename on delete or download of cloud backup * Improve tests * Address review comments
This commit is contained in:
parent
51aea58c7a
commit
4cdc3de94a
@ -11,7 +11,11 @@ from typing import Any
|
|||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from hass_nabucasa import Cloud, CloudError
|
from hass_nabucasa import Cloud, CloudError
|
||||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||||
from hass_nabucasa.cloud_api import async_files_delete_file, async_files_list
|
from hass_nabucasa.cloud_api import (
|
||||||
|
FilesHandlerListEntry,
|
||||||
|
async_files_delete_file,
|
||||||
|
async_files_list,
|
||||||
|
)
|
||||||
from hass_nabucasa.files import FilesError, StorageType, calculate_b64md5
|
from hass_nabucasa.files import FilesError, StorageType, calculate_b64md5
|
||||||
|
|
||||||
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
||||||
@ -76,11 +80,6 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@callback
|
|
||||||
def _get_backup_filename(self) -> str:
|
|
||||||
"""Return the backup filename."""
|
|
||||||
return f"{self._cloud.client.prefs.instance_id}.tar"
|
|
||||||
|
|
||||||
async def async_download_backup(
|
async def async_download_backup(
|
||||||
self,
|
self,
|
||||||
backup_id: str,
|
backup_id: str,
|
||||||
@ -91,13 +90,13 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||||
:return: An async iterator that yields bytes.
|
:return: An async iterator that yields bytes.
|
||||||
"""
|
"""
|
||||||
if not await self.async_get_backup(backup_id):
|
if not (backup := await self._async_get_backup(backup_id)):
|
||||||
raise BackupAgentError("Backup not found")
|
raise BackupAgentError("Backup not found")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = await self._cloud.files.download(
|
content = await self._cloud.files.download(
|
||||||
storage_type=StorageType.BACKUP,
|
storage_type=StorageType.BACKUP,
|
||||||
filename=self._get_backup_filename(),
|
filename=backup["Key"],
|
||||||
)
|
)
|
||||||
except CloudError as err:
|
except CloudError as err:
|
||||||
raise BackupAgentError(f"Failed to download backup: {err}") from err
|
raise BackupAgentError(f"Failed to download backup: {err}") from err
|
||||||
@ -124,7 +123,7 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
base64md5hash = await calculate_b64md5(open_stream, size)
|
base64md5hash = await calculate_b64md5(open_stream, size)
|
||||||
except FilesError as err:
|
except FilesError as err:
|
||||||
raise BackupAgentError(err) from err
|
raise BackupAgentError(err) from err
|
||||||
filename = self._get_backup_filename()
|
filename = f"{self._cloud.client.prefs.instance_id}.tar"
|
||||||
metadata = backup.as_dict()
|
metadata = backup.as_dict()
|
||||||
|
|
||||||
tries = 1
|
tries = 1
|
||||||
@ -172,29 +171,34 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
|
|
||||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||||
"""
|
"""
|
||||||
if not await self.async_get_backup(backup_id):
|
if not (backup := await self._async_get_backup(backup_id)):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await async_files_delete_file(
|
await async_files_delete_file(
|
||||||
self._cloud,
|
self._cloud,
|
||||||
storage_type=StorageType.BACKUP,
|
storage_type=StorageType.BACKUP,
|
||||||
filename=self._get_backup_filename(),
|
filename=backup["Key"],
|
||||||
)
|
)
|
||||||
except (ClientError, CloudError) as err:
|
except (ClientError, CloudError) as err:
|
||||||
raise BackupAgentError("Failed to delete backup") from err
|
raise BackupAgentError("Failed to delete backup") from err
|
||||||
|
|
||||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||||
|
"""List backups."""
|
||||||
|
backups = await self._async_list_backups()
|
||||||
|
return [AgentBackup.from_dict(backup["Metadata"]) for backup in backups]
|
||||||
|
|
||||||
|
async def _async_list_backups(self) -> list[FilesHandlerListEntry]:
|
||||||
"""List backups."""
|
"""List backups."""
|
||||||
try:
|
try:
|
||||||
backups = await async_files_list(
|
backups = await async_files_list(
|
||||||
self._cloud, storage_type=StorageType.BACKUP
|
self._cloud, storage_type=StorageType.BACKUP
|
||||||
)
|
)
|
||||||
_LOGGER.debug("Cloud backups: %s", backups)
|
|
||||||
except (ClientError, CloudError) as err:
|
except (ClientError, CloudError) as err:
|
||||||
raise BackupAgentError("Failed to list backups") from err
|
raise BackupAgentError("Failed to list backups") from err
|
||||||
|
|
||||||
return [AgentBackup.from_dict(backup["Metadata"]) for backup in backups]
|
_LOGGER.debug("Cloud backups: %s", backups)
|
||||||
|
return backups
|
||||||
|
|
||||||
async def async_get_backup(
|
async def async_get_backup(
|
||||||
self,
|
self,
|
||||||
@ -202,10 +206,19 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> AgentBackup | None:
|
) -> AgentBackup | None:
|
||||||
"""Return a backup."""
|
"""Return a backup."""
|
||||||
backups = await self.async_list_backups()
|
if not (backup := await self._async_get_backup(backup_id)):
|
||||||
|
return None
|
||||||
|
return AgentBackup.from_dict(backup["Metadata"])
|
||||||
|
|
||||||
|
async def _async_get_backup(
|
||||||
|
self,
|
||||||
|
backup_id: str,
|
||||||
|
) -> FilesHandlerListEntry | None:
|
||||||
|
"""Return a backup."""
|
||||||
|
backups = await self._async_list_backups()
|
||||||
|
|
||||||
for backup in backups:
|
for backup in backups:
|
||||||
if backup.backup_id == backup_id:
|
if backup["Metadata"]["backup_id"] == backup_id:
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
from collections.abc import AsyncGenerator, Generator
|
from collections.abc import AsyncGenerator, Generator
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import Mock, PropertyMock, patch
|
from unittest.mock import ANY, Mock, PropertyMock, patch
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from hass_nabucasa import CloudError
|
from hass_nabucasa import CloudError
|
||||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||||
from hass_nabucasa.files import FilesError
|
from hass_nabucasa.files import FilesError, StorageType
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.backup import (
|
from homeassistant.components.backup import (
|
||||||
@ -90,7 +90,26 @@ def mock_list_files() -> Generator[MagicMock]:
|
|||||||
"size": 34519040,
|
"size": 34519040,
|
||||||
"storage-type": "backup",
|
"storage-type": "backup",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"Key": "462e16810d6841228828d9dd2f9e341f.tar",
|
||||||
|
"LastModified": "2024-11-22T10:49:01.182Z",
|
||||||
|
"Size": 34519040,
|
||||||
|
"Metadata": {
|
||||||
|
"addons": [],
|
||||||
|
"backup_id": "23e64aed",
|
||||||
|
"date": "2024-11-22T11:48:48.727189+01:00",
|
||||||
|
"database_included": True,
|
||||||
|
"extra_metadata": {},
|
||||||
|
"folders": [],
|
||||||
|
"homeassistant_included": True,
|
||||||
|
"homeassistant_version": "2024.12.0.dev0",
|
||||||
|
"name": "Core 2024.12.0.dev0",
|
||||||
|
"protected": False,
|
||||||
|
"size": 34519040,
|
||||||
|
"storage-type": "backup",
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
yield list_files
|
yield list_files
|
||||||
|
|
||||||
@ -148,7 +167,21 @@ async def test_agents_list_backups(
|
|||||||
"name": "Core 2024.12.0.dev0",
|
"name": "Core 2024.12.0.dev0",
|
||||||
"failed_agent_ids": [],
|
"failed_agent_ids": [],
|
||||||
"with_automatic_settings": None,
|
"with_automatic_settings": None,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"addons": [],
|
||||||
|
"agents": {"cloud.cloud": {"protected": False, "size": 34519040}},
|
||||||
|
"backup_id": "23e64aed",
|
||||||
|
"date": "2024-11-22T11:48:48.727189+01:00",
|
||||||
|
"database_included": True,
|
||||||
|
"extra_metadata": {},
|
||||||
|
"folders": [],
|
||||||
|
"homeassistant_included": True,
|
||||||
|
"homeassistant_version": "2024.12.0.dev0",
|
||||||
|
"name": "Core 2024.12.0.dev0",
|
||||||
|
"failed_agent_ids": [],
|
||||||
|
"with_automatic_settings": None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -242,6 +275,10 @@ async def test_agents_download(
|
|||||||
resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud")
|
resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert await resp.content.read() == b"backup data"
|
assert await resp.content.read() == b"backup data"
|
||||||
|
cloud.files.download.assert_called_once_with(
|
||||||
|
filename="462e16810d6841228828d9dd2f9e341e.tar",
|
||||||
|
storage_type=StorageType.BACKUP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
|
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
|
||||||
@ -317,7 +354,14 @@ async def test_agents_upload(
|
|||||||
data={"file": StringIO(backup_data)},
|
data={"file": StringIO(backup_data)},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(cloud.files.upload.mock_calls) == 1
|
cloud.files.upload.assert_called_once_with(
|
||||||
|
storage_type=StorageType.BACKUP,
|
||||||
|
open_stream=ANY,
|
||||||
|
filename=f"{cloud.client.prefs.instance_id}.tar",
|
||||||
|
base64md5hash=ANY,
|
||||||
|
metadata=ANY,
|
||||||
|
size=ANY,
|
||||||
|
)
|
||||||
metadata = cloud.files.upload.mock_calls[-1].kwargs["metadata"]
|
metadata = cloud.files.upload.mock_calls[-1].kwargs["metadata"]
|
||||||
assert metadata["backup_id"] == backup_id
|
assert metadata["backup_id"] == backup_id
|
||||||
|
|
||||||
@ -552,6 +596,7 @@ async def test_agents_upload_wrong_size(
|
|||||||
async def test_agents_delete(
|
async def test_agents_delete(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
cloud: Mock,
|
||||||
mock_delete_file: Mock,
|
mock_delete_file: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test agent delete backup."""
|
"""Test agent delete backup."""
|
||||||
@ -568,7 +613,11 @@ async def test_agents_delete(
|
|||||||
|
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {"agent_errors": {}}
|
assert response["result"] == {"agent_errors": {}}
|
||||||
mock_delete_file.assert_called_once()
|
mock_delete_file.assert_called_once_with(
|
||||||
|
cloud,
|
||||||
|
filename="462e16810d6841228828d9dd2f9e341e.tar",
|
||||||
|
storage_type=StorageType.BACKUP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side_effect", [ClientError, CloudError])
|
@pytest.mark.parametrize("side_effect", [ClientError, CloudError])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user