diff --git a/supervisor/api/backups.py b/supervisor/api/backups.py index b37671bd9e3..b1bb445fe21 100644 --- a/supervisor/api/backups.py +++ b/supervisor/api/backups.py @@ -26,6 +26,7 @@ ATTR_DATE, ATTR_DAYS_UNTIL_STALE, ATTR_EXTRA, + ATTR_FILENAME, ATTR_FOLDERS, ATTR_HOMEASSISTANT, ATTR_HOMEASSISTANT_EXCLUDE_DATABASE, @@ -98,6 +99,7 @@ def _ensure_list(item: Any) -> list: SCHEMA_BACKUP_FULL = vol.Schema( { vol.Optional(ATTR_NAME): str, + vol.Optional(ATTR_FILENAME): str, vol.Optional(ATTR_PASSWORD): vol.Maybe(str), vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()), vol.Optional(ATTR_LOCATION): vol.All( diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index 1ebd477004e..83d0514e5b3 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -184,6 +184,7 @@ def _list_backup_files(self, path: Path) -> Iterable[Path]: def _create_backup( self, name: str, + filename: str | None, sys_type: BackupType, password: str | None, compressed: bool = True, @@ -196,7 +197,11 @@ def _create_backup( """ 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 @@ async def _do_backup( async def do_backup_full( self, name: str = "", + filename: str | None = None, *, password: str | None = None, compressed: bool = True, @@ -500,7 +506,7 @@ async def do_backup_full( ) 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 @@ async def do_backup_full( 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 @@ async def do_backup_partial( _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) diff --git a/tests/backups/test_backup.py b/tests/backups/test_backup.py index 45e1e90b978..44224efaef2 100644 --- a/tests/backups/test_backup.py +++ b/tests/backups/test_backup.py @@ -19,4 +19,4 @@ async def test_new_backup_stays_in_folder(coresys: CoreSys, tmp_path: Path): assert backup.tarfile.exists() assert len(listdir(tmp_path)) == 1 - assert backup.tarfile.exists() + assert backup.tarfile.exists() \ No newline at end of file diff --git a/tests/backups/test_manager.py b/tests/backups/test_manager.py index 52dcefa8537..e753ae69aed 100644 --- a/tests/backups/test_manager.py +++ b/tests/backups/test_manager.py @@ -73,6 +73,31 @@ 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 ):