From 463f196472ce084e13bbe0a6bc4f73c083be7727 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:18:55 +0100 Subject: [PATCH] Bump securetar from 2024.11.0 to 2025.1.3 (#5553) * Bump securetar from 2024.11.0 to 2025.1.3 Bumps [securetar](https://github.com/pvizeli/securetar) from 2024.11.0 to 2025.1.3. - [Release notes](https://github.com/pvizeli/securetar/releases) - [Commits](https://github.com/pvizeli/securetar/compare/2024.11.0...2025.1.3) --- updated-dependencies: - dependency-name: securetar dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Use file_filter and add test for addon backup_exclude --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mike Degatano --- requirements.txt | 2 +- supervisor/addons/addon.py | 28 ++++++++++++++++++++++++++-- supervisor/backups/backup.py | 24 ++++++++++++++++++------ supervisor/homeassistant/module.py | 14 +++++++++++++- tests/backups/test_manager.py | 26 ++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 755c0710a..75483e30b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ pulsectl==24.12.0 pyudev==0.24.3 PyYAML==6.0.2 requests==2.32.3 -securetar==2024.11.0 +securetar==2025.1.3 sentry-sdk==2.20.0 setuptools==75.8.0 voluptuous==0.15.2 diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 53fb92d4b..2802e0d9f 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -6,6 +6,7 @@ from contextlib import suppress from copy import deepcopy from datetime import datetime import errno +from functools import partial from ipaddress import IPv4Address import logging from pathlib import Path, PurePath @@ -1207,6 +1208,25 @@ class Addon(AddonModel): await self._backup_command(self.backup_post) return None + def _is_excluded_by_filter( + self, origin_path: Path, arcname: str, item_arcpath: PurePath + ) -> bool: + """Filter out files from backup based on filters provided by addon developer. + + This tests the dev provided filters against the full path of the file as + Supervisor sees them using match. This is done for legacy reasons, testing + against the relative path makes more sense and may be changed in the future. + """ + full_path = origin_path / item_arcpath.relative_to(arcname) + + for exclude in self.backup_exclude: + if not full_path.match(exclude): + continue + _LOGGER.debug("Ignoring %s because of %s", full_path, exclude) + return True + + return False + @Job( name="addon_backup", limit=JobExecutionLimit.GROUP_ONCE, @@ -1266,7 +1286,9 @@ class Addon(AddonModel): atomic_contents_add( backup, self.path_data, - excludes=self.backup_exclude, + file_filter=partial( + self._is_excluded_by_filter, self.path_data, "data" + ), arcname="data", ) @@ -1275,7 +1297,9 @@ class Addon(AddonModel): atomic_contents_add( backup, self.path_config, - excludes=self.backup_exclude, + file_filter=partial( + self._is_excluded_by_filter, self.path_config, "config" + ), arcname="config", ) diff --git a/supervisor/backups/backup.py b/supervisor/backups/backup.py index ef0b038ea..4f935ea87 100644 --- a/supervisor/backups/backup.py +++ b/supervisor/backups/backup.py @@ -11,7 +11,7 @@ from functools import cached_property import io import json import logging -from pathlib import Path +from pathlib import Path, PurePath import tarfile from tarfile import TarFile from tempfile import TemporaryDirectory @@ -640,6 +640,22 @@ class Backup(JobGroup): # Take backup _LOGGER.info("Backing up folder %s", name) + def is_excluded_by_filter(item_arcpath: PurePath) -> bool: + """Filter out bind mounts in folders being backed up.""" + full_path = origin_dir / item_arcpath.relative_to(".") + + for bound in self.sys_mounts.bound_mounts: + if full_path != bound.bind_mount.local_where: + continue + _LOGGER.debug( + "Ignoring %s because of %s", + full_path, + bound.bind_mount.local_where.as_posix(), + ) + return True + + return False + with self._outer_secure_tarfile.create_inner_tar( f"./{tar_name}", gzip=self.compressed, @@ -648,11 +664,7 @@ class Backup(JobGroup): atomic_contents_add( tar_file, origin_dir, - excludes=[ - bound.bind_mount.local_where.as_posix() - for bound in self.sys_mounts.bound_mounts - if bound.bind_mount.local_where - ], + file_filter=is_excluded_by_filter, arcname=".", ) diff --git a/supervisor/homeassistant/module.py b/supervisor/homeassistant/module.py index 3d4a17484..003a58e3b 100644 --- a/supervisor/homeassistant/module.py +++ b/supervisor/homeassistant/module.py @@ -416,11 +416,23 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes): if exclude_database: excludes += HOMEASSISTANT_BACKUP_EXCLUDE_DATABASE + def is_excluded_by_filter(path: PurePath) -> bool: + """Filter to filter excludes.""" + for exclude in excludes: + if not path.match(exclude): + continue + _LOGGER.debug( + "Ignoring %s because of %s", path, exclude + ) + return True + + return False + # Backup data atomic_contents_add( backup, self.sys_config.path_homeassistant, - excludes=excludes, + file_filter=is_excluded_by_filter, arcname="data", ) diff --git a/tests/backups/test_manager.py b/tests/backups/test_manager.py index 90e3ebb73..52dcefa85 100644 --- a/tests/backups/test_manager.py +++ b/tests/backups/test_manager.py @@ -2013,3 +2013,29 @@ async def test_backup_remove_one_location_of_multiple(coresys: CoreSys): assert not location_2.exists() assert coresys.backups.get("7fed74c8") assert backup.all_locations == {None: location_1} + + +@pytest.mark.usefixtures("tmp_supervisor_data") +async def test_addon_backup_excludes(coresys: CoreSys, install_addon_example: Addon): + """Test backup excludes option for addons.""" + coresys.core.state = CoreState.RUNNING + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + + install_addon_example.path_data.mkdir(parents=True) + (test1 := install_addon_example.path_data / "test1").touch() + (test_dir := install_addon_example.path_data / "test_dir").mkdir() + (test2 := test_dir / "test2").touch() + (test3 := test_dir / "test3").touch() + + install_addon_example.data["backup_exclude"] = ["test1", "*/test2"] + backup = await coresys.backups.do_backup_partial(addons=["local_example"]) + test1.unlink() + test2.unlink() + test3.unlink() + test_dir.rmdir() + + await coresys.backups.do_restore_partial(backup, addons=["local_example"]) + assert not test1.exists() + assert not test2.exists() + assert test_dir.is_dir() + assert test3.exists()