mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-08 17:56:33 +00:00
Extend backup API with file name field (#5567)
* Extend backup API with file name field Allow to specify a backup file name when creating a backup. This allows for user friendly backup file names. If none is specified, the current behavior remains (backup file name is the backup slug). * Check passed file name using regex * Use custom filename on download only if backup file name is backup slug * ruff format * Remove path from location for download file name
This commit is contained in:
parent
a545b680b3
commit
088832c253
@ -26,6 +26,7 @@ from ..const import (
|
||||
ATTR_DATE,
|
||||
ATTR_DAYS_UNTIL_STALE,
|
||||
ATTR_EXTRA,
|
||||
ATTR_FILENAME,
|
||||
ATTR_FOLDERS,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
|
||||
@ -98,6 +99,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||
SCHEMA_BACKUP_FULL = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_NAME): str,
|
||||
vol.Optional(ATTR_FILENAME): vol.Match(RE_BACKUP_FILENAME),
|
||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
||||
vol.Optional(ATTR_LOCATION): vol.All(
|
||||
@ -458,10 +460,15 @@ class APIBackups(CoreSysAttributes):
|
||||
raise APIError(f"Backup {backup.slug} is not in location {location}")
|
||||
|
||||
_LOGGER.info("Downloading backup %s", backup.slug)
|
||||
response = web.FileResponse(backup.all_locations[location])
|
||||
filename = backup.all_locations[location]
|
||||
response = web.FileResponse(filename)
|
||||
response.content_type = CONTENT_TYPE_TAR
|
||||
|
||||
download_filename = filename.name
|
||||
if download_filename == f"{backup.slug}.tar":
|
||||
download_filename = f"{RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||
response.headers[CONTENT_DISPOSITION] = (
|
||||
f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||
f"attachment; filename={download_filename}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
@ -184,6 +184,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
def _create_backup(
|
||||
self,
|
||||
name: str,
|
||||
filename: str | None,
|
||||
sys_type: BackupType,
|
||||
password: str | None,
|
||||
compressed: bool = True,
|
||||
@ -196,7 +197,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
"""
|
||||
date_str = utcnow().isoformat()
|
||||
slug = create_slug(name, date_str)
|
||||
tar_file = Path(self._get_base_path(location), f"{slug}.tar")
|
||||
|
||||
if filename:
|
||||
tar_file = Path(self._get_base_path(location), Path(filename).name)
|
||||
else:
|
||||
tar_file = Path(self._get_base_path(location), f"{slug}.tar")
|
||||
|
||||
# init object
|
||||
backup = Backup(self.coresys, tar_file, slug, self._get_location_name(location))
|
||||
@ -482,6 +487,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
async def do_backup_full(
|
||||
self,
|
||||
name: str = "",
|
||||
filename: str | None = None,
|
||||
*,
|
||||
password: str | None = None,
|
||||
compressed: bool = True,
|
||||
@ -500,7 +506,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
)
|
||||
|
||||
backup = self._create_backup(
|
||||
name, BackupType.FULL, password, compressed, location, extra
|
||||
name, filename, BackupType.FULL, password, compressed, location, extra
|
||||
)
|
||||
|
||||
_LOGGER.info("Creating new full backup with slug %s", backup.slug)
|
||||
@ -526,6 +532,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
async def do_backup_partial(
|
||||
self,
|
||||
name: str = "",
|
||||
filename: str | None = None,
|
||||
*,
|
||||
addons: list[str] | None = None,
|
||||
folders: list[str] | None = None,
|
||||
@ -558,7 +565,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
_LOGGER.error("Nothing to create backup for")
|
||||
|
||||
backup = self._create_backup(
|
||||
name, BackupType.PARTIAL, password, compressed, location, extra
|
||||
name, filename, BackupType.PARTIAL, password, compressed, location, extra
|
||||
)
|
||||
|
||||
_LOGGER.info("Creating new partial backup with slug %s", backup.slug)
|
||||
|
@ -73,6 +73,28 @@ async def test_do_backup_full(coresys: CoreSys, backup_mock, install_addon_ssh):
|
||||
assert coresys.core.state == CoreState.RUNNING
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("filename", "filename_expected"),
|
||||
[("../my file.tar", "/data/backup/my file.tar"), (None, "/data/backup/{}.tar")],
|
||||
)
|
||||
async def test_do_backup_full_with_filename(
|
||||
coresys: CoreSys, filename: str, filename_expected: str, backup_mock
|
||||
):
|
||||
"""Test creating Backup with a specific file name."""
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||
|
||||
manager = BackupManager(coresys)
|
||||
|
||||
# backup_mock fixture causes Backup() to be a MagicMock
|
||||
await manager.do_backup_full(filename=filename)
|
||||
|
||||
slug = backup_mock.call_args[0][2]
|
||||
assert str(backup_mock.call_args[0][1]) == filename_expected.format(slug)
|
||||
|
||||
assert coresys.core.state == CoreState.RUNNING
|
||||
|
||||
|
||||
async def test_do_backup_full_uncompressed(
|
||||
coresys: CoreSys, backup_mock, install_addon_ssh
|
||||
):
|
||||
|
Loading…
x
Reference in New Issue
Block a user