Skip to content

Commit

Permalink
Use file_filter and add test for addon backup_exclude
Browse files Browse the repository at this point in the history
  • Loading branch information
mdegat01 committed Jan 16, 2025
1 parent 2a56c28 commit fb50692
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 9 deletions.
28 changes: 26 additions & 2 deletions supervisor/addons/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand Down Expand Up @@ -1207,6 +1208,25 @@ async def end_backup(self) -> asyncio.Task | None:
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,
Expand Down Expand Up @@ -1266,7 +1286,9 @@ def _write_tarfile():
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",
)

Expand All @@ -1275,7 +1297,9 @@ def _write_tarfile():
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",
)

Expand Down
24 changes: 18 additions & 6 deletions supervisor/backups/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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
Expand Down Expand Up @@ -640,6 +640,22 @@ def _save() -> None:
# 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,
Expand All @@ -648,11 +664,7 @@ def _save() -> None:
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=".",
)

Expand Down
14 changes: 13 additions & 1 deletion supervisor/homeassistant/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,23 @@ def _write_tarfile():
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",
)

Expand Down
26 changes: 26 additions & 0 deletions tests/backups/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit fb50692

Please sign in to comment.