1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-19 18:38:58 +00:00
Files
core/homeassistant/components/sftp_storage/backup.py
Marko Todorić 0fecf012e6 SFTP/SSH as remote Backup location (#135844)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-05 15:43:26 +02:00

154 lines
5.2 KiB
Python

"""Backup platform for the SFTP Storage integration."""
from __future__ import annotations
from collections.abc import AsyncIterator, Callable, Coroutine
from typing import Any
from asyncssh.sftp import SFTPError
from homeassistant.components.backup import (
AgentBackup,
BackupAgent,
BackupAgentError,
BackupNotFound,
)
from homeassistant.core import HomeAssistant, callback
from . import SFTPConfigEntry
from .client import BackupAgentClient
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN, LOGGER
async def async_get_backup_agents(
hass: HomeAssistant,
) -> list[BackupAgent]:
"""Register the backup agents."""
entries: list[SFTPConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
return [SFTPBackupAgent(hass, entry) for entry in entries]
@callback
def async_register_backup_agents_listener(
hass: HomeAssistant,
*,
listener: Callable[[], None],
**kwargs: Any,
) -> Callable[[], None]:
"""Register a listener to be called when agents are added or removed."""
hass.data.setdefault(DATA_BACKUP_AGENT_LISTENERS, []).append(listener)
@callback
def remove_listener() -> None:
"""Remove the listener."""
hass.data[DATA_BACKUP_AGENT_LISTENERS].remove(listener)
if not hass.data[DATA_BACKUP_AGENT_LISTENERS]:
del hass.data[DATA_BACKUP_AGENT_LISTENERS]
return remove_listener
class SFTPBackupAgent(BackupAgent):
"""SFTP Backup Storage agent."""
domain = DOMAIN
def __init__(self, hass: HomeAssistant, entry: SFTPConfigEntry) -> None:
"""Initialize the SFTPBackupAgent backup sync agent."""
super().__init__()
self._entry: SFTPConfigEntry = entry
self._hass: HomeAssistant = hass
self.name: str = entry.title
self.unique_id: str = entry.entry_id
async def async_download_backup(
self,
backup_id: str,
**kwargs: Any,
) -> AsyncIterator[bytes]:
"""Download a backup file from SFTP."""
LOGGER.debug(
"Establishing SFTP connection to remote host in order to download backup id: %s",
backup_id,
)
try:
# Will raise BackupAgentError if failure to authenticate or SFTP Permissions
async with BackupAgentClient(self._entry, self._hass) as client:
return await client.iter_file(backup_id)
except FileNotFoundError as e:
raise BackupNotFound(
f"Unable to initiate download of backup id: {backup_id}. {e}"
) from e
async def async_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
**kwargs: Any,
) -> None:
"""Upload a backup."""
LOGGER.debug("Received request to upload backup: %s", backup)
iterator = await open_stream()
LOGGER.debug(
"Establishing SFTP connection to remote host in order to upload backup"
)
# Will raise BackupAgentError if failure to authenticate or SFTP Permissions
async with BackupAgentClient(self._entry, self._hass) as client:
LOGGER.debug("Uploading backup: %s", backup.backup_id)
await client.async_upload_backup(iterator, backup)
LOGGER.debug("Successfully uploaded backup id: %s", backup.backup_id)
async def async_delete_backup(
self,
backup_id: str,
**kwargs: Any,
) -> None:
"""Delete a backup file from SFTP Storage."""
LOGGER.debug("Received request to delete backup id: %s", backup_id)
try:
LOGGER.debug(
"Establishing SFTP connection to remote host in order to delete backup"
)
# Will raise BackupAgentError if failure to authenticate or SFTP Permissions
async with BackupAgentClient(self._entry, self._hass) as client:
await client.async_delete_backup(backup_id)
except FileNotFoundError as err:
raise BackupNotFound(str(err)) from err
except SFTPError as err:
raise BackupAgentError(
f"Failed to delete backup id: {backup_id}: {err}"
) from err
LOGGER.debug("Successfully removed backup id: %s", backup_id)
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
"""List backups stored on SFTP Storage."""
# Will raise BackupAgentError if failure to authenticate or SFTP Permissions
async with BackupAgentClient(self._entry, self._hass) as client:
try:
return await client.async_list_backups()
except SFTPError as err:
raise BackupAgentError(
f"Remote server error while attempting to list backups: {err}"
) from err
async def async_get_backup(
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup:
"""Return a backup."""
backups = await self.async_list_backups()
for backup in backups:
if backup.backup_id == backup_id:
LOGGER.debug("Returning backup id: %s. %s", backup_id, backup)
return backup
raise BackupNotFound(f"Backup id: {backup_id} not found")