mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 00:07:10 +00:00
Make supervisor backup file names more user friendly (#137020)
This commit is contained in:
parent
e0bf248867
commit
64f679ba8f
@ -35,6 +35,7 @@ from .manager import (
|
|||||||
WrittenBackup,
|
WrittenBackup,
|
||||||
)
|
)
|
||||||
from .models import AddonInfo, AgentBackup, Folder
|
from .models import AddonInfo, AgentBackup, Folder
|
||||||
|
from .util import suggested_filename, suggested_filename_from_name_date
|
||||||
from .websocket import async_register_websocket_handlers
|
from .websocket import async_register_websocket_handlers
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -58,6 +59,8 @@ __all__ = [
|
|||||||
"RestoreBackupState",
|
"RestoreBackupState",
|
||||||
"WrittenBackup",
|
"WrittenBackup",
|
||||||
"async_get_manager",
|
"async_get_manager",
|
||||||
|
"suggested_filename",
|
||||||
|
"suggested_filename_from_name_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
@ -118,12 +118,15 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def suggested_filename_from_name_date(name: str, date_str: str) -> str:
|
||||||
|
"""Suggest a filename for the backup."""
|
||||||
|
date = dt_util.parse_datetime(date_str, raise_on_error=True)
|
||||||
|
return "_".join(f"{name} - {date.strftime('%Y-%m-%d %H.%M %S%f')}.tar".split())
|
||||||
|
|
||||||
|
|
||||||
def suggested_filename(backup: AgentBackup) -> str:
|
def suggested_filename(backup: AgentBackup) -> str:
|
||||||
"""Suggest a filename for the backup."""
|
"""Suggest a filename for the backup."""
|
||||||
date = dt_util.parse_datetime(backup.date, raise_on_error=True)
|
return suggested_filename_from_name_date(backup.name, backup.date)
|
||||||
return "_".join(
|
|
||||||
f"{backup.name} - {date.strftime('%Y-%m-%d %H.%M %S%f')}.tar".split()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_password(path: Path, password: str | None) -> bool:
|
def validate_password(path: Path, password: str | None) -> bool:
|
||||||
|
@ -6,7 +6,7 @@ import asyncio
|
|||||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path, PurePath
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -38,11 +38,14 @@ from homeassistant.components.backup import (
|
|||||||
RestoreBackupState,
|
RestoreBackupState,
|
||||||
WrittenBackup,
|
WrittenBackup,
|
||||||
async_get_manager as async_get_backup_manager,
|
async_get_manager as async_get_backup_manager,
|
||||||
|
suggested_filename as suggested_backup_filename,
|
||||||
|
suggested_filename_from_name_date,
|
||||||
)
|
)
|
||||||
from homeassistant.const import __version__ as HAVERSION
|
from homeassistant.const import __version__ as HAVERSION
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN, EVENT_SUPERVISOR_EVENT
|
from .const import DOMAIN, EVENT_SUPERVISOR_EVENT
|
||||||
from .handler import get_supervisor_client
|
from .handler import get_supervisor_client
|
||||||
@ -113,12 +116,15 @@ def _backup_details_to_agent_backup(
|
|||||||
AddonInfo(name=addon.name, slug=addon.slug, version=addon.version)
|
AddonInfo(name=addon.name, slug=addon.slug, version=addon.version)
|
||||||
for addon in details.addons
|
for addon in details.addons
|
||||||
]
|
]
|
||||||
|
extra_metadata = details.extra or {}
|
||||||
location = location or LOCATION_LOCAL
|
location = location or LOCATION_LOCAL
|
||||||
return AgentBackup(
|
return AgentBackup(
|
||||||
addons=addons,
|
addons=addons,
|
||||||
backup_id=details.slug,
|
backup_id=details.slug,
|
||||||
database_included=database_included,
|
database_included=database_included,
|
||||||
date=details.date.isoformat(),
|
date=extra_metadata.get(
|
||||||
|
"supervisor.backup_request_date", details.date.isoformat()
|
||||||
|
),
|
||||||
extra_metadata=details.extra or {},
|
extra_metadata=details.extra or {},
|
||||||
folders=[Folder(folder) for folder in details.folders],
|
folders=[Folder(folder) for folder in details.folders],
|
||||||
homeassistant_included=homeassistant_included,
|
homeassistant_included=homeassistant_included,
|
||||||
@ -174,7 +180,8 @@ class SupervisorBackupAgent(BackupAgent):
|
|||||||
return
|
return
|
||||||
stream = await open_stream()
|
stream = await open_stream()
|
||||||
upload_options = supervisor_backups.UploadBackupOptions(
|
upload_options = supervisor_backups.UploadBackupOptions(
|
||||||
location={self.location}
|
location={self.location},
|
||||||
|
filename=PurePath(suggested_backup_filename(backup)),
|
||||||
)
|
)
|
||||||
await self._client.backups.upload_backup(
|
await self._client.backups.upload_backup(
|
||||||
stream,
|
stream,
|
||||||
@ -301,6 +308,9 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
locations = []
|
locations = []
|
||||||
locations = locations or [LOCATION_CLOUD_BACKUP]
|
locations = locations or [LOCATION_CLOUD_BACKUP]
|
||||||
|
|
||||||
|
date = dt_util.now().isoformat()
|
||||||
|
extra_metadata = extra_metadata | {"supervisor.backup_request_date": date}
|
||||||
|
filename = suggested_filename_from_name_date(backup_name, date)
|
||||||
try:
|
try:
|
||||||
backup = await self._client.backups.partial_backup(
|
backup = await self._client.backups.partial_backup(
|
||||||
supervisor_backups.PartialBackupOptions(
|
supervisor_backups.PartialBackupOptions(
|
||||||
@ -314,6 +324,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
homeassistant_exclude_database=not include_database,
|
homeassistant_exclude_database=not include_database,
|
||||||
background=True,
|
background=True,
|
||||||
extra=extra_metadata,
|
extra=extra_metadata,
|
||||||
|
filename=PurePath(filename),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except SupervisorError as err:
|
except SupervisorError as err:
|
||||||
|
@ -11,6 +11,7 @@ from dataclasses import replace
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import os
|
import os
|
||||||
|
from pathlib import PurePath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@ -26,6 +27,7 @@ from aiohasupervisor.models import (
|
|||||||
mounts as supervisor_mounts,
|
mounts as supervisor_mounts,
|
||||||
)
|
)
|
||||||
from aiohasupervisor.models.mounts import MountsInfo
|
from aiohasupervisor.models.mounts import MountsInfo
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.backup import (
|
from homeassistant.components.backup import (
|
||||||
@ -854,8 +856,10 @@ DEFAULT_BACKUP_OPTIONS = supervisor_backups.PartialBackupOptions(
|
|||||||
compressed=True,
|
compressed=True,
|
||||||
extra={
|
extra={
|
||||||
"instance_id": ANY,
|
"instance_id": ANY,
|
||||||
|
"supervisor.backup_request_date": "2025-01-30T05:42:12.345678-08:00",
|
||||||
"with_automatic_settings": False,
|
"with_automatic_settings": False,
|
||||||
},
|
},
|
||||||
|
filename=PurePath("Test_-_2025-01-30_05.42_12345678.tar"),
|
||||||
folders={"ssl"},
|
folders={"ssl"},
|
||||||
homeassistant_exclude_database=False,
|
homeassistant_exclude_database=False,
|
||||||
homeassistant=True,
|
homeassistant=True,
|
||||||
@ -907,12 +911,14 @@ DEFAULT_BACKUP_OPTIONS = supervisor_backups.PartialBackupOptions(
|
|||||||
async def test_reader_writer_create(
|
async def test_reader_writer_create(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
supervisor_client: AsyncMock,
|
supervisor_client: AsyncMock,
|
||||||
extra_generate_options: dict[str, Any],
|
extra_generate_options: dict[str, Any],
|
||||||
expected_supervisor_options: supervisor_backups.PartialBackupOptions,
|
expected_supervisor_options: supervisor_backups.PartialBackupOptions,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test generating a backup."""
|
"""Test generating a backup."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
freezer.move_to("2025-01-30 13:42:12.345678")
|
||||||
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
||||||
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
|
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
|
||||||
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
|
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
|
||||||
@ -982,10 +988,12 @@ async def test_reader_writer_create(
|
|||||||
async def test_reader_writer_create_job_done(
|
async def test_reader_writer_create_job_done(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
supervisor_client: AsyncMock,
|
supervisor_client: AsyncMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test generating a backup, and backup job finishes early."""
|
"""Test generating a backup, and backup job finishes early."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
freezer.move_to("2025-01-30 13:42:12.345678")
|
||||||
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
||||||
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
|
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
|
||||||
supervisor_client.jobs.get_job.return_value = TEST_JOB_DONE
|
supervisor_client.jobs.get_job.return_value = TEST_JOB_DONE
|
||||||
@ -1140,6 +1148,7 @@ async def test_reader_writer_create_job_done(
|
|||||||
async def test_reader_writer_create_per_agent_encryption(
|
async def test_reader_writer_create_per_agent_encryption(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
supervisor_client: AsyncMock,
|
supervisor_client: AsyncMock,
|
||||||
commands: dict[str, Any],
|
commands: dict[str, Any],
|
||||||
password: str | None,
|
password: str | None,
|
||||||
@ -1151,6 +1160,7 @@ async def test_reader_writer_create_per_agent_encryption(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test generating a backup."""
|
"""Test generating a backup."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
freezer.move_to("2025-01-30 13:42:12.345678")
|
||||||
mounts = MountsInfo(
|
mounts = MountsInfo(
|
||||||
default_backup_mount=None,
|
default_backup_mount=None,
|
||||||
mounts=[
|
mounts=[
|
||||||
@ -1170,6 +1180,7 @@ async def test_reader_writer_create_per_agent_encryption(
|
|||||||
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
||||||
supervisor_client.backups.backup_info.return_value = replace(
|
supervisor_client.backups.backup_info.return_value = replace(
|
||||||
TEST_BACKUP_DETAILS,
|
TEST_BACKUP_DETAILS,
|
||||||
|
extra=DEFAULT_BACKUP_OPTIONS.extra,
|
||||||
locations=create_locations,
|
locations=create_locations,
|
||||||
location_attributes={
|
location_attributes={
|
||||||
location or LOCATION_LOCAL: supervisor_backups.BackupLocationAttributes(
|
location or LOCATION_LOCAL: supervisor_backups.BackupLocationAttributes(
|
||||||
@ -1254,6 +1265,7 @@ async def test_reader_writer_create_per_agent_encryption(
|
|||||||
upload_locations
|
upload_locations
|
||||||
)
|
)
|
||||||
for call in supervisor_client.backups.upload_backup.mock_calls:
|
for call in supervisor_client.backups.upload_backup.mock_calls:
|
||||||
|
assert call.args[1].filename == PurePath("Test_-_2025-01-30_05.42_12345678.tar")
|
||||||
upload_call_locations: set = call.args[1].location
|
upload_call_locations: set = call.args[1].location
|
||||||
assert len(upload_call_locations) == 1
|
assert len(upload_call_locations) == 1
|
||||||
assert upload_call_locations.pop() in upload_locations
|
assert upload_call_locations.pop() in upload_locations
|
||||||
@ -1569,10 +1581,12 @@ async def test_reader_writer_create_info_error(
|
|||||||
async def test_reader_writer_create_remote_backup(
|
async def test_reader_writer_create_remote_backup(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
supervisor_client: AsyncMock,
|
supervisor_client: AsyncMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test generating a backup which will be uploaded to a remote agent."""
|
"""Test generating a backup which will be uploaded to a remote agent."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
freezer.move_to("2025-01-30 13:42:12.345678")
|
||||||
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
|
||||||
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS_5
|
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS_5
|
||||||
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
|
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
|
||||||
|
Loading…
x
Reference in New Issue
Block a user