Refactor BackupManager (#130947)

* Refactor BackupManager

* Adjust

* Adjust backup creation

* Copy in executor
This commit is contained in:
Erik Montnemery
2024-11-19 19:09:15 +01:00
committed by GitHub
parent 4a44d19c53
commit 20488721bd
6 changed files with 232 additions and 155 deletions

View File

@@ -11,7 +11,12 @@ from homeassistant.helpers.typing import ConfigType
from .agent import BackupAgent, BackupAgentPlatformProtocol from .agent import BackupAgent, BackupAgentPlatformProtocol
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .http import async_register_http_views from .http import async_register_http_views
from .manager import Backup, BackupManager, BackupPlatformProtocol from .manager import (
Backup,
BackupManager,
BackupPlatformProtocol,
CoreBackupReaderWriter,
)
from .models import BackupUploadMetadata, BaseBackup from .models import BackupUploadMetadata, BaseBackup
from .websocket import async_register_websocket_handlers from .websocket import async_register_websocket_handlers
@@ -31,7 +36,9 @@ SERVICE_CREATE_SCHEMA = vol.Schema({vol.Optional(CONF_PASSWORD): str})
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Backup integration.""" """Set up the Backup integration."""
hass.data[DOMAIN] = backup_manager = BackupManager(hass) hass.data[DOMAIN] = backup_manager = BackupManager(
hass, CoreBackupReaderWriter(hass)
)
await backup_manager.async_setup() await backup_manager.async_setup()
with_hassio = is_hassio(hass) with_hassio = is_hassio(hass)

View File

@@ -8,12 +8,11 @@ from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING: if TYPE_CHECKING:
from .manager import BaseBackupManager from .manager import BackupManager
from .models import BaseBackup
BUF_SIZE = 2**20 * 4 # 4MB BUF_SIZE = 2**20 * 4 # 4MB
DOMAIN = "backup" DOMAIN = "backup"
DATA_MANAGER: HassKey[BaseBackupManager[BaseBackup]] = HassKey(DOMAIN) DATA_MANAGER: HassKey[BackupManager] = HassKey(DOMAIN)
LOGGER = getLogger(__package__) LOGGER = getLogger(__package__)
EXCLUDE_FROM_BACKUP = [ EXCLUDE_FROM_BACKUP = [

View File

@@ -15,7 +15,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import DATA_MANAGER from .const import DATA_MANAGER
from .manager import BackupManager
# pylint: disable=fixme # pylint: disable=fixme
# TODO: Don't forget to remove this when the implementation is complete # TODO: Don't forget to remove this when the implementation is complete
@@ -47,7 +46,7 @@ class DownloadBackupView(HomeAssistantView):
except KeyError: except KeyError:
return Response(status=HTTPStatus.BAD_REQUEST) return Response(status=HTTPStatus.BAD_REQUEST)
manager = cast(BackupManager, request.app[KEY_HASS].data[DATA_MANAGER]) manager = request.app[KEY_HASS].data[DATA_MANAGER]
if agent_id not in manager.backup_agents: if agent_id not in manager.backup_agents:
return Response(status=HTTPStatus.BAD_REQUEST) return Response(status=HTTPStatus.BAD_REQUEST)
agent = manager.backup_agents[agent_id] agent = manager.backup_agents[agent_id]

View File

@@ -15,11 +15,10 @@ import shutil
import tarfile import tarfile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import time import time
from typing import Any, Generic, Protocol from typing import TYPE_CHECKING, Any, Protocol
import aiohttp import aiohttp
from securetar import SecureTarFile, atomic_contents_add from securetar import SecureTarFile, atomic_contents_add
from typing_extensions import TypeVar
from homeassistant.backup_restore import RESTORE_BACKUP_FILE, password_to_key from homeassistant.backup_restore import RESTORE_BACKUP_FILE, password_to_key
from homeassistant.const import __version__ as HAVERSION from homeassistant.const import __version__ as HAVERSION
@@ -38,6 +37,7 @@ from .agent import (
from .config import BackupConfig from .config import BackupConfig
from .const import ( from .const import (
BUF_SIZE, BUF_SIZE,
DATA_MANAGER,
DOMAIN, DOMAIN,
EXCLUDE_DATABASE_FROM_BACKUP, EXCLUDE_DATABASE_FROM_BACKUP,
EXCLUDE_FROM_BACKUP, EXCLUDE_FROM_BACKUP,
@@ -46,8 +46,6 @@ from .const import (
from .models import BackupUploadMetadata, BaseBackup from .models import BackupUploadMetadata, BaseBackup
from .util import read_backup from .util import read_backup
_BackupT = TypeVar("_BackupT", bound=BaseBackup, default=BaseBackup)
@dataclass(slots=True) @dataclass(slots=True)
class NewBackup: class NewBackup:
@@ -82,24 +80,62 @@ class BackupPlatformProtocol(Protocol):
"""Perform operations after a backup finishes.""" """Perform operations after a backup finishes."""
class BaseBackupManager(abc.ABC, Generic[_BackupT]): class BackupReaderWriter(abc.ABC):
"""Abstract class for reading and writing backups."""
temp_backup_dir: Path
@abc.abstractmethod
async def async_create_backup(
self,
*,
addons_included: list[str] | None,
agent_ids: list[str],
database_included: bool,
backup_name: str,
folders_included: list[str] | None,
on_progress: Callable[[BackupProgress], None] | None,
password: str | None,
) -> tuple[NewBackup, asyncio.Task[tuple[BaseBackup, Path]]]:
"""Create a backup."""
@abc.abstractmethod
async def async_restore_backup(
self,
backup_id: str,
*,
agent_id: str,
password: str | None,
**kwargs: Any,
) -> None:
"""Restore a backup."""
class BackupManager:
"""Define the format that backup managers can have.""" """Define the format that backup managers can have."""
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant, reader_writer: BackupReaderWriter) -> None:
"""Initialize the backup manager.""" """Initialize the backup manager."""
self.hass = hass self.hass = hass
self.backup_task: asyncio.Task | None = None self.backup_task: asyncio.Task[tuple[BaseBackup, Path]] | None = None
self.finish_backup_task: asyncio.Task[None] | None = None
self.platforms: dict[str, BackupPlatformProtocol] = {} self.platforms: dict[str, BackupPlatformProtocol] = {}
self.backup_agents: dict[str, BackupAgent] = {} self.backup_agents: dict[str, BackupAgent] = {}
self.local_backup_agents: dict[str, LocalBackupAgent] = {} self.local_backup_agents: dict[str, LocalBackupAgent] = {}
self.config = BackupConfig(hass) self.config = BackupConfig(hass)
self.syncing = False self.syncing = False
self._reader_writer = reader_writer
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up the backup manager.""" """Set up the backup manager."""
await self.config.load() await self.config.load()
await self.load_platforms() await self.load_platforms()
@property
def temp_backup_dir(self) -> Path:
"""Return the temporary backup directory."""
return self._reader_writer.temp_backup_dir
@callback @callback
def _add_platform_pre_post_handler( def _add_platform_pre_post_handler(
self, self,
@@ -182,74 +218,6 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
LOGGER.debug("Loaded %s platforms", len(self.platforms)) LOGGER.debug("Loaded %s platforms", len(self.platforms))
LOGGER.debug("Loaded %s agents", len(self.backup_agents)) LOGGER.debug("Loaded %s agents", len(self.backup_agents))
@abc.abstractmethod
async def async_restore_backup(
self,
backup_id: str,
*,
agent_id: str,
password: str | None,
**kwargs: Any,
) -> None:
"""Restore a backup."""
@abc.abstractmethod
async def async_create_backup(
self,
*,
addons_included: list[str] | None,
agent_ids: list[str],
database_included: bool,
folders_included: list[str] | None,
name: str | None,
on_progress: Callable[[BackupProgress], None] | None,
password: str | None,
**kwargs: Any,
) -> NewBackup:
"""Initiate generating a backup.
:param on_progress: A callback that will be called with the progress of the
backup.
"""
@abc.abstractmethod
async def async_get_backups(
self, **kwargs: Any
) -> tuple[dict[str, Backup], dict[str, Exception]]:
"""Get backups.
Return a dictionary of Backup instances keyed by their ID.
"""
@abc.abstractmethod
async def async_get_backup(
self, backup_id: str, **kwargs: Any
) -> tuple[_BackupT | None, dict[str, Exception]]:
"""Get a backup."""
@abc.abstractmethod
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
"""Delete a backup."""
@abc.abstractmethod
async def async_receive_backup(
self,
*,
agent_ids: list[str],
contents: aiohttp.BodyPartReader,
**kwargs: Any,
) -> None:
"""Receive and store a backup file from upload."""
class BackupManager(BaseBackupManager[Backup]):
"""Backup manager for the Backup integration."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup manager."""
super().__init__(hass=hass)
self.temp_backup_dir = Path(hass.config.path("tmp_backups"))
async def _async_upload_backup( async def _async_upload_backup(
self, self,
*, *,
@@ -288,7 +256,10 @@ class BackupManager(BaseBackupManager[Backup]):
async def async_get_backups( async def async_get_backups(
self, **kwargs: Any self, **kwargs: Any
) -> tuple[dict[str, Backup], dict[str, Exception]]: ) -> tuple[dict[str, Backup], dict[str, Exception]]:
"""Return backups.""" """Get backups.
Return a dictionary of Backup instances keyed by their ID.
"""
backups: dict[str, Backup] = {} backups: dict[str, Backup] = {}
agent_errors: dict[str, Exception] = {} agent_errors: dict[str, Exception] = {}
agent_ids = list(self.backup_agents) agent_ids = list(self.backup_agents)
@@ -320,7 +291,7 @@ class BackupManager(BaseBackupManager[Backup]):
async def async_get_backup( async def async_get_backup(
self, backup_id: str, **kwargs: Any self, backup_id: str, **kwargs: Any
) -> tuple[Backup | None, dict[str, Exception]]: ) -> tuple[Backup | None, dict[str, Exception]]:
"""Return a backup.""" """Get a backup."""
backup: Backup | None = None backup: Backup | None = None
agent_errors: dict[str, Exception] = {} agent_errors: dict[str, Exception] = {}
agent_ids = list(self.backup_agents.keys()) agent_ids = list(self.backup_agents.keys())
@@ -453,7 +424,11 @@ class BackupManager(BaseBackupManager[Backup]):
password: str | None, password: str | None,
**kwargs: Any, **kwargs: Any,
) -> NewBackup: ) -> NewBackup:
"""Initiate generating a backup.""" """Initiate generating a backup.
:param on_progress: A callback that will be called with the progress of the
backup.
"""
if self.backup_task: if self.backup_task:
raise HomeAssistantError("Backup already in progress") raise HomeAssistantError("Backup already in progress")
if not agent_ids: if not agent_ids:
@@ -461,9 +436,118 @@ class BackupManager(BaseBackupManager[Backup]):
if any(agent_id not in self.backup_agents for agent_id in agent_ids): if any(agent_id not in self.backup_agents for agent_id in agent_ids):
raise HomeAssistantError("Invalid agent selected") raise HomeAssistantError("Invalid agent selected")
backup_name = name or f"Core {HAVERSION}" backup_name = name or f"Core {HAVERSION}"
new_backup, self.backup_task = await self._reader_writer.async_create_backup(
addons_included=addons_included,
agent_ids=agent_ids,
backup_name=backup_name,
database_included=database_included,
folders_included=folders_included,
on_progress=on_progress,
password=password,
)
self.finish_backup_task = self.hass.async_create_task(
self._async_finish_backup(agent_ids),
name="backup_manager_finish_backup",
)
return new_backup
async def _async_finish_backup(self, agent_ids: list[str]) -> None:
if TYPE_CHECKING:
assert self.backup_task is not None
try:
backup, tar_file_path = await self.backup_task
except Exception as err: # noqa: BLE001
LOGGER.debug("Backup upload failed", exc_info=err)
else:
LOGGER.debug(
"Generated new backup with backup_id %s, uploading to agents %s",
backup.backup_id,
agent_ids,
)
local_file_paths = [
self.local_backup_agents[agent_id].get_backup_path(backup.backup_id)
for agent_id in agent_ids
if agent_id in self.local_backup_agents
]
keep_path = False
for local_path in local_file_paths:
if local_path == tar_file_path:
keep_path = True
continue
await self.hass.async_add_executor_job(
shutil.copy, tar_file_path, local_path
)
await self._async_upload_backup(
backup=backup, agent_ids=agent_ids, path=tar_file_path
)
if not keep_path:
await self.hass.async_add_executor_job(tar_file_path.unlink, True)
finally:
self.backup_task = None
self.finish_backup_task = None
async def async_restore_backup(
self,
backup_id: str,
*,
agent_id: str,
password: str | None,
**kwargs: Any,
) -> None:
"""Initiate restoring a backup.
:param on_progress: A callback that will be called with the progress of the
restore. Home Assistant Core may need to be restarted during the backup
restore process, which means the restore process may not be able to report
when it's done.
"""
if agent_id in self.local_backup_agents:
local_agent = self.local_backup_agents[agent_id]
if not await local_agent.async_get_backup(backup_id):
raise HomeAssistantError(
f"Backup {backup_id} not found in agent {agent_id}"
)
else:
path = self.temp_backup_dir / f"{backup_id}.tar"
agent = self.backup_agents[agent_id]
if not await agent.async_get_backup(backup_id):
raise HomeAssistantError(
f"Backup {backup_id} not found in agent {agent_id}"
)
await agent.async_download_backup(backup_id, path=path)
await self._reader_writer.async_restore_backup(
backup_id=backup_id,
agent_id=agent_id,
password=password,
)
class CoreBackupReaderWriter(BackupReaderWriter):
"""Class for reading and writing backups in core and container installations."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup reader/writer."""
self._hass = hass
self.temp_backup_dir = Path(hass.config.path("tmp_backups"))
async def async_create_backup(
self,
*,
addons_included: list[str] | None,
agent_ids: list[str],
database_included: bool,
backup_name: str,
folders_included: list[str] | None,
on_progress: Callable[[BackupProgress], None] | None,
password: str | None,
) -> tuple[NewBackup, asyncio.Task[tuple[BaseBackup, Path]]]:
"""Initiate generating a backup."""
date_str = dt_util.now().isoformat() date_str = dt_util.now().isoformat()
backup_id = _generate_backup_id(date_str, backup_name) backup_id = _generate_backup_id(date_str, backup_name)
self.backup_task = self.hass.async_create_task(
backup_task = self._hass.async_create_task(
self._async_create_backup( self._async_create_backup(
addons_included=addons_included, addons_included=addons_included,
agent_ids=agent_ids, agent_ids=agent_ids,
@@ -478,32 +562,35 @@ class BackupManager(BaseBackupManager[Backup]):
name="backup_manager_create_backup", name="backup_manager_create_backup",
eager_start=False, # To ensure the task is not started before we return eager_start=False, # To ensure the task is not started before we return
) )
return NewBackup(backup_id=backup_id)
return (NewBackup(backup_id=backup_id), backup_task)
async def _async_create_backup( async def _async_create_backup(
self, self,
*, *,
addons_included: list[str] | None, addons_included: list[str] | None,
agent_ids: list[str], agent_ids: list[str],
backup_id: str,
database_included: bool, database_included: bool,
backup_name: str, backup_name: str,
date_str: str, date_str: str,
folders_included: list[str] | None, folders_included: list[str] | None,
on_progress: Callable[[BackupProgress], None] | None, on_progress: Callable[[BackupProgress], None] | None,
password: str | None, password: str | None,
backup_id: str, ) -> tuple[BaseBackup, Path]:
) -> BaseBackup:
"""Generate a backup.""" """Generate a backup."""
manager = self._hass.data[DATA_MANAGER]
success = False success = False
local_file_paths = [ suggested_tar_file_path = None
self.local_backup_agents[agent_id].get_backup_path(backup_id) for agent_id in agent_ids:
for agent_id in agent_ids if local_agent := manager.local_backup_agents.get(agent_id):
if agent_id in self.local_backup_agents suggested_tar_file_path = local_agent.get_backup_path(backup_id)
] break
try: try:
await self.async_pre_backup_actions() # Inform integrations a backup is about to be made
await manager.async_pre_backup_actions()
backup_data = { backup_data = {
"compressed": True, "compressed": True,
@@ -519,12 +606,12 @@ class BackupManager(BaseBackupManager[Backup]):
"type": "partial", "type": "partial",
} }
tar_file_path, size_in_bytes = await self.hass.async_add_executor_job( tar_file_path, size_in_bytes = await self._hass.async_add_executor_job(
self._mkdir_and_generate_backup_contents, self._mkdir_and_generate_backup_contents,
local_file_paths,
backup_data, backup_data,
database_included, database_included,
password, password,
suggested_tar_file_path,
) )
backup = BaseBackup( backup = BaseBackup(
backup_id=backup_id, backup_id=backup_id,
@@ -533,35 +620,23 @@ class BackupManager(BaseBackupManager[Backup]):
protected=password is not None, protected=password is not None,
size=round(size_in_bytes / 1_048_576, 2), size=round(size_in_bytes / 1_048_576, 2),
) )
LOGGER.debug(
"Generated new backup with backup_id %s, uploading to agents %s",
backup_id,
agent_ids,
)
await self._async_upload_backup(
backup=backup, agent_ids=agent_ids, path=tar_file_path
)
if not local_file_paths:
await self.hass.async_add_executor_job(tar_file_path.unlink, True)
success = True success = True
return backup return (backup, tar_file_path)
finally: finally:
if on_progress: if on_progress:
on_progress(BackupProgress(done=True, stage=None, success=success)) on_progress(BackupProgress(done=True, stage=None, success=success))
self.backup_task = None # Inform integrations the backup is done
await self.async_post_backup_actions() await manager.async_post_backup_actions()
def _mkdir_and_generate_backup_contents( def _mkdir_and_generate_backup_contents(
self, self,
tar_file_paths: list[Path],
backup_data: dict[str, Any], backup_data: dict[str, Any],
database_included: bool, database_included: bool,
password: str | None, password: str | None,
tar_file_path: Path | None,
) -> tuple[Path, int]: ) -> tuple[Path, int]:
"""Generate backup contents and return the size.""" """Generate backup contents and return the size."""
if tar_file_paths: if not tar_file_path:
tar_file_path = tar_file_paths[0]
else:
tar_file_path = self.temp_backup_dir / f"{backup_data['slug']}.tar" tar_file_path = self.temp_backup_dir / f"{backup_data['slug']}.tar"
if not (backup_dir := tar_file_path.parent).exists(): if not (backup_dir := tar_file_path.parent).exists():
LOGGER.debug("Creating backup directory %s", backup_dir) LOGGER.debug("Creating backup directory %s", backup_dir)
@@ -588,12 +663,10 @@ class BackupManager(BaseBackupManager[Backup]):
) as core_tar: ) as core_tar:
atomic_contents_add( atomic_contents_add(
tar_file=core_tar, tar_file=core_tar,
origin_path=Path(self.hass.config.path()), origin_path=Path(self._hass.config.path()),
excludes=excludes, excludes=excludes,
arcname="data", arcname="data",
) )
for local_path in tar_file_paths[1:]:
shutil.copy(tar_file_path, local_path)
return (tar_file_path, tar_file_path.stat().st_size) return (tar_file_path, tar_file_path.stat().st_size)
async def async_restore_backup( async def async_restore_backup(
@@ -610,31 +683,22 @@ class BackupManager(BaseBackupManager[Backup]):
will be handled during startup by the restore_backup module. will be handled during startup by the restore_backup module.
""" """
if agent_id in self.local_backup_agents: manager = self._hass.data[DATA_MANAGER]
local_agent = self.local_backup_agents[agent_id] if agent_id in manager.local_backup_agents:
if not await local_agent.async_get_backup(backup_id): local_agent = manager.local_backup_agents[agent_id]
raise HomeAssistantError(
f"Backup {backup_id} not found in agent {agent_id}"
)
path = local_agent.get_backup_path(backup_id) path = local_agent.get_backup_path(backup_id)
else: else:
path = self.temp_backup_dir / f"{backup_id}.tar" path = self.temp_backup_dir / f"{backup_id}.tar"
agent = self.backup_agents[agent_id]
if not await agent.async_get_backup(backup_id):
raise HomeAssistantError(
f"Backup {backup_id} not found in agent {agent_id}"
)
await agent.async_download_backup(backup_id, path=path)
def _write_restore_file() -> None: def _write_restore_file() -> None:
"""Write the restore file.""" """Write the restore file."""
Path(self.hass.config.path(RESTORE_BACKUP_FILE)).write_text( Path(self._hass.config.path(RESTORE_BACKUP_FILE)).write_text(
json.dumps({"path": path.as_posix(), "password": password}), json.dumps({"path": path.as_posix(), "password": password}),
encoding="utf-8", encoding="utf-8",
) )
await self.hass.async_add_executor_job(_write_restore_file) await self._hass.async_add_executor_job(_write_restore_file)
await self.hass.services.async_call("homeassistant", "restart", {}) await self._hass.services.async_call("homeassistant", "restart", {})
def _generate_backup_id(date: str, name: str) -> str: def _generate_backup_id(date: str, name: str) -> str:

View File

@@ -1,6 +1,6 @@
"""Websocket commands for the Backup integration.""" """Websocket commands for the Backup integration."""
from typing import Any, cast from typing import Any
import voluptuous as vol import voluptuous as vol
@@ -8,7 +8,7 @@ from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import DATA_MANAGER, LOGGER from .const import DATA_MANAGER, LOGGER
from .manager import BackupManager, BackupProgress from .manager import BackupProgress
@callback @callback
@@ -254,7 +254,7 @@ async def backup_agents_download(
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Download an uploaded backup.""" """Download an uploaded backup."""
manager = cast(BackupManager, hass.data[DATA_MANAGER]) manager = hass.data[DATA_MANAGER]
if not (agent := manager.backup_agents.get(msg["agent_id"])): if not (agent := manager.backup_agents.get(msg["agent_id"])):
connection.send_error( connection.send_error(
msg["id"], "unknown_agent", f"Agent {msg['agent_id']} not found" msg["id"], "unknown_agent", f"Agent {msg['agent_id']} not found"

View File

@@ -18,7 +18,11 @@ from homeassistant.components.backup import (
BaseBackup, BaseBackup,
backup as local_backup_platform, backup as local_backup_platform,
) )
from homeassistant.components.backup.manager import BackupProgress from homeassistant.components.backup.const import DATA_MANAGER
from homeassistant.components.backup.manager import (
BackupProgress,
CoreBackupReaderWriter,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@@ -71,7 +75,8 @@ async def _mock_backup_generation(
assert manager.backup_task is not None assert manager.backup_task is not None
assert progress == [] assert progress == []
backup = await manager.backup_task backup, _ = await manager.backup_task
await manager.finish_backup_task
assert progress == [BackupProgress(done=True, stage=None, success=True)] assert progress == [BackupProgress(done=True, stage=None, success=True)]
assert mocked_json_bytes.call_count == 1 assert mocked_json_bytes.call_count == 1
@@ -132,13 +137,13 @@ async def _setup_backup_platform(
async def test_constructor(hass: HomeAssistant) -> None: async def test_constructor(hass: HomeAssistant) -> None:
"""Test BackupManager constructor.""" """Test BackupManager constructor."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
assert manager.temp_backup_dir.as_posix() == hass.config.path("tmp_backups") assert manager.temp_backup_dir.as_posix() == hass.config.path("tmp_backups")
async def test_load_backups(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: async def test_load_backups(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None:
"""Test loading backups.""" """Test loading backups."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -170,7 +175,7 @@ async def test_load_backups_with_exception(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test loading backups with exception.""" """Test loading backups with exception."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -194,7 +199,7 @@ async def test_deleting_backup(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test deleting backup.""" """Test deleting backup."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -213,7 +218,7 @@ async def test_deleting_non_existing_backup(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test deleting not existing backup.""" """Test deleting not existing backup."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -227,7 +232,7 @@ async def test_getting_backup_that_does_not_exist(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test getting backup that does not exist.""" """Test getting backup that does not exist."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -253,7 +258,7 @@ async def test_getting_backup_that_does_not_exist(
async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None: async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None:
"""Test generate backup.""" """Test generate backup."""
event = asyncio.Event() event = asyncio.Event()
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
manager.backup_task = hass.async_create_task(event.wait()) manager.backup_task = hass.async_create_task(event.wait())
with pytest.raises(HomeAssistantError, match="Backup already in progress"): with pytest.raises(HomeAssistantError, match="Backup already in progress"):
await manager.async_create_backup( await manager.async_create_backup(
@@ -279,7 +284,7 @@ async def test_async_create_backup_wrong_agent_id(
hass: HomeAssistant, agent_ids: list[str], expected_error: str hass: HomeAssistant, agent_ids: list[str], expected_error: str
) -> None: ) -> None:
"""Test generate backup.""" """Test generate backup."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
with pytest.raises(HomeAssistantError, match=expected_error): with pytest.raises(HomeAssistantError, match=expected_error):
await manager.async_create_backup( await manager.async_create_backup(
addons_included=[], addons_included=[],
@@ -320,7 +325,8 @@ async def test_async_create_backup(
backup_directory: str, backup_directory: str,
) -> None: ) -> None:
"""Test generate backup.""" """Test generate backup."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
hass.data[DATA_MANAGER] = manager
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await _setup_backup_platform( await _setup_backup_platform(
@@ -358,7 +364,7 @@ async def test_loading_platforms(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test loading backup platforms.""" """Test loading backup platforms."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
assert not manager.platforms assert not manager.platforms
@@ -383,7 +389,7 @@ async def test_loading_agents(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test loading backup agents.""" """Test loading backup agents."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
assert not manager.platforms assert not manager.platforms
@@ -407,7 +413,7 @@ async def test_not_loading_bad_platforms(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test loading backup platforms.""" """Test loading backup platforms."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
assert not manager.platforms assert not manager.platforms
@@ -424,7 +430,7 @@ async def test_exception_plaform_pre(
hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock
) -> None: ) -> None:
"""Test exception in pre step.""" """Test exception in pre step."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
manager.loaded_backups = True manager.loaded_backups = True
async def _mock_step(hass: HomeAssistant) -> None: async def _mock_step(hass: HomeAssistant) -> None:
@@ -447,7 +453,7 @@ async def test_exception_plaform_post(
hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock
) -> None: ) -> None:
"""Test exception in post step.""" """Test exception in post step."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
manager.loaded_backups = True manager.loaded_backups = True
async def _mock_step(hass: HomeAssistant) -> None: async def _mock_step(hass: HomeAssistant) -> None:
@@ -471,7 +477,7 @@ async def test_async_receive_backup(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test receiving a backup file.""" """Test receiving a backup file."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -516,7 +522,8 @@ async def test_async_trigger_restore(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test trigger restore.""" """Test trigger restore."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
hass.data[DATA_MANAGER] = manager
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -545,7 +552,8 @@ async def test_async_trigger_restore_with_password(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test trigger restore.""" """Test trigger restore."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
hass.data[DATA_MANAGER] = manager
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()
@@ -573,7 +581,7 @@ async def test_async_trigger_restore_with_password(
async def test_async_trigger_restore_missing_backup(hass: HomeAssistant) -> None: async def test_async_trigger_restore_missing_backup(hass: HomeAssistant) -> None:
"""Test trigger restore.""" """Test trigger restore."""
manager = BackupManager(hass) manager = BackupManager(hass, CoreBackupReaderWriter(hass))
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform) await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
await manager.load_platforms() await manager.load_platforms()