Check if backup is still available when accessing

When accessing a backup (e.g. to restore, download or delete it) check
if the locations of the backup file are still available. If not, force
a backup reload instead.
This commit is contained in:
Stefan Agner 2025-01-02 22:20:44 +01:00
parent f02d67ee47
commit b68d381c22
No known key found for this signature in database
GPG Key ID: AE01353D1E44747D
2 changed files with 27 additions and 8 deletions

View File

@ -143,11 +143,29 @@ SCHEMA_REMOVE = vol.Schema(
class APIBackups(CoreSysAttributes):
"""Handle RESTful API for backups functions."""
def _extract_slug(self, request):
async def _get_backup_by_slug(self, request):
"""Return backup, throw an exception if it doesn't exist."""
backup = self.sys_backups.get(request.match_info.get("slug"))
if not backup:
raise APINotFound("Backup does not exist")
# Reload in case locations of this backup are no longer in sync
# This usually means the user
def _check_need_reload():
for location in backup.all_locations:
if not Path(location).exists():
return False
return True
if not await self.sys_run_in_executor(_check_need_reload):
_LOGGER.warning(
"Backup %s missing in at least one location, scheduling reload",
backup.slug,
)
# Schedule a reload
self.sys_jobs.schedule_job(self.sys_backups.reload, JobSchedulerOptions())
raise APINotFound("Backup does not exist")
return backup
def _list_backups(self):
@ -212,7 +230,7 @@ class APIBackups(CoreSysAttributes):
@api_process
async def backup_info(self, request):
"""Return backup info."""
backup = self._extract_slug(request)
backup = await self._get_backup_by_slug(request)
data_addons = []
for addon_data in backup.addons:
@ -384,7 +402,7 @@ class APIBackups(CoreSysAttributes):
@api_process
async def restore_full(self, request: web.Request):
"""Full restore of a backup."""
backup = self._extract_slug(request)
backup = await self._get_backup_by_slug(request)
body = await api_validate(SCHEMA_RESTORE_FULL, request)
self._validate_cloud_backup_location(
request, body.get(ATTR_LOCATION, backup.location)
@ -404,7 +422,7 @@ class APIBackups(CoreSysAttributes):
@api_process
async def restore_partial(self, request: web.Request):
"""Partial restore a backup."""
backup = self._extract_slug(request)
backup = await self._get_backup_by_slug(request)
body = await api_validate(SCHEMA_RESTORE_PARTIAL, request)
self._validate_cloud_backup_location(
request, body.get(ATTR_LOCATION, backup.location)
@ -435,7 +453,7 @@ class APIBackups(CoreSysAttributes):
@api_process
async def remove(self, request: web.Request):
"""Remove a backup."""
backup = self._extract_slug(request)
backup = await self._get_backup_by_slug(request)
body = await api_validate(SCHEMA_REMOVE, request)
locations: list[LOCATION_TYPE] | None = None
@ -450,7 +468,7 @@ class APIBackups(CoreSysAttributes):
@api_process
async def download(self, request: web.Request):
"""Download a backup file."""
backup = self._extract_slug(request)
backup = self._get_backup_by_slug(request)
# Query will give us '' for /backups, convert value to None
location = request.query.get(ATTR_LOCATION, backup.location) or None
self._validate_cloud_backup_location(request, location)

View File

@ -200,18 +200,19 @@ class BackupManager(FileConfiguration, JobGroup):
return backup
def load(self) -> Awaitable[None]:
"""Load exists backups data.
"""Load existing backups data.
Return a coroutine.
"""
return self.reload()
@Job(name="backup_reload")
async def reload(
self,
location: LOCATION_TYPE | type[DEFAULT] = DEFAULT,
filename: str | None = None,
) -> bool:
"""Load exists backups."""
"""Load existing backups."""
async def _load_backup(location: str | None, tar_file: Path) -> bool:
"""Load the backup."""