mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-09 02:06:30 +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_DATE,
|
||||||
ATTR_DAYS_UNTIL_STALE,
|
ATTR_DAYS_UNTIL_STALE,
|
||||||
ATTR_EXTRA,
|
ATTR_EXTRA,
|
||||||
|
ATTR_FILENAME,
|
||||||
ATTR_FOLDERS,
|
ATTR_FOLDERS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
|
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
|
||||||
@ -98,6 +99,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
|||||||
SCHEMA_BACKUP_FULL = vol.Schema(
|
SCHEMA_BACKUP_FULL = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_NAME): str,
|
vol.Optional(ATTR_NAME): str,
|
||||||
|
vol.Optional(ATTR_FILENAME): vol.Match(RE_BACKUP_FILENAME),
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
||||||
vol.Optional(ATTR_LOCATION): vol.All(
|
vol.Optional(ATTR_LOCATION): vol.All(
|
||||||
@ -458,10 +460,15 @@ class APIBackups(CoreSysAttributes):
|
|||||||
raise APIError(f"Backup {backup.slug} is not in location {location}")
|
raise APIError(f"Backup {backup.slug} is not in location {location}")
|
||||||
|
|
||||||
_LOGGER.info("Downloading backup %s", backup.slug)
|
_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
|
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] = (
|
response.headers[CONTENT_DISPOSITION] = (
|
||||||
f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
f"attachment; filename={download_filename}"
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -184,6 +184,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
def _create_backup(
|
def _create_backup(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
filename: str | None,
|
||||||
sys_type: BackupType,
|
sys_type: BackupType,
|
||||||
password: str | None,
|
password: str | None,
|
||||||
compressed: bool = True,
|
compressed: bool = True,
|
||||||
@ -196,7 +197,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
"""
|
"""
|
||||||
date_str = utcnow().isoformat()
|
date_str = utcnow().isoformat()
|
||||||
slug = create_slug(name, date_str)
|
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
|
# init object
|
||||||
backup = Backup(self.coresys, tar_file, slug, self._get_location_name(location))
|
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(
|
async def do_backup_full(
|
||||||
self,
|
self,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
|
filename: str | None = None,
|
||||||
*,
|
*,
|
||||||
password: str | None = None,
|
password: str | None = None,
|
||||||
compressed: bool = True,
|
compressed: bool = True,
|
||||||
@ -500,7 +506,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
backup = self._create_backup(
|
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)
|
_LOGGER.info("Creating new full backup with slug %s", backup.slug)
|
||||||
@ -526,6 +532,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
async def do_backup_partial(
|
async def do_backup_partial(
|
||||||
self,
|
self,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
|
filename: str | None = None,
|
||||||
*,
|
*,
|
||||||
addons: list[str] | None = None,
|
addons: list[str] | None = None,
|
||||||
folders: list[str] | None = None,
|
folders: list[str] | None = None,
|
||||||
@ -558,7 +565,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
_LOGGER.error("Nothing to create backup for")
|
_LOGGER.error("Nothing to create backup for")
|
||||||
|
|
||||||
backup = self._create_backup(
|
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)
|
_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
|
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(
|
async def test_do_backup_full_uncompressed(
|
||||||
coresys: CoreSys, backup_mock, install_addon_ssh
|
coresys: CoreSys, backup_mock, install_addon_ssh
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user