Don't backup uninstalled addons (#5988)

* Don't backup uninstalled addons

* Remove hash in backup
This commit is contained in:
Mike Degatano 2025-07-04 01:05:53 -04:00 committed by GitHub
parent f0ea0d4a44
commit ae036ceffe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 7 deletions

View File

@ -63,6 +63,8 @@ from .const import BUF_SIZE, LOCATION_CLOUD_BACKUP, BackupType
from .utils import password_to_key
from .validate import SCHEMA_BACKUP
IGNORED_COMPARISON_FIELDS = {ATTR_PROTECTED, ATTR_CRYPTO, ATTR_DOCKER}
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -265,7 +267,7 @@ class Backup(JobGroup):
# Compare all fields except ones about protection. Current encryption status does not affect equality
keys = self._data.keys() | other._data.keys()
for k in keys - {ATTR_PROTECTED, ATTR_CRYPTO, ATTR_DOCKER}:
for k in keys - IGNORED_COMPARISON_FIELDS:
if (
k not in self._data
or k not in other._data
@ -577,13 +579,21 @@ class Backup(JobGroup):
@Job(name="backup_addon_save", cleanup=False)
async def _addon_save(self, addon: Addon) -> asyncio.Task | None:
"""Store an add-on into backup."""
self.sys_jobs.current.reference = addon.slug
self.sys_jobs.current.reference = slug = addon.slug
if not self._outer_secure_tarfile:
raise RuntimeError(
"Cannot backup components without initializing backup tar"
)
tar_name = f"{addon.slug}.tar{'.gz' if self.compressed else ''}"
# Ensure it is still installed and get current data before proceeding
if not (curr_addon := self.sys_addons.get_local_only(slug)):
_LOGGER.warning(
"Skipping backup of add-on %s because it has been uninstalled",
slug,
)
return None
tar_name = f"{slug}.tar{'.gz' if self.compressed else ''}"
addon_file = self._outer_secure_tarfile.create_inner_tar(
f"./{tar_name}",
@ -592,16 +602,16 @@ class Backup(JobGroup):
)
# Take backup
try:
start_task = await addon.backup(addon_file)
start_task = await curr_addon.backup(addon_file)
except AddonsError as err:
raise BackupError(str(err)) from err
# Store to config
self._data[ATTR_ADDONS].append(
{
ATTR_SLUG: addon.slug,
ATTR_NAME: addon.name,
ATTR_VERSION: addon.version,
ATTR_SLUG: slug,
ATTR_NAME: curr_addon.name,
ATTR_VERSION: curr_addon.version,
# Bug - addon_file.size used to give us this information
# It always returns 0 in current securetar. Skipping until fixed
ATTR_SIZE: 0,

View File

@ -2244,3 +2244,33 @@ async def test_get_upload_path_for_mount_location(
result = await manager.get_upload_path_for_location(mount)
assert result == mount.local_where
@pytest.mark.usefixtures(
"supervisor_internet", "tmp_supervisor_data", "path_extern", "install_addon_example"
)
async def test_backup_addon_skips_uninstalled(
coresys: CoreSys, caplog: pytest.LogCaptureFixture
):
"""Test restore installing new addon."""
await coresys.core.set_state(CoreState.RUNNING)
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
assert "local_example" in coresys.addons.local
orig_store_addons = Backup.store_addons
async def mock_store_addons(*args, **kwargs):
# Mock an uninstall during the backup process
await coresys.addons.uninstall("local_example")
await orig_store_addons(*args, **kwargs)
with patch.object(Backup, "store_addons", new=mock_store_addons):
backup: Backup = await coresys.backups.do_backup_partial(
addons=["local_example"], folders=["ssl"]
)
assert "local_example" not in coresys.addons.local
assert not backup.addons
assert (
"Skipping backup of add-on local_example because it has been uninstalled"
in caplog.text
)