From b68d381c22d60c6693333102b975fef93814ad91 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 2 Jan 2025 22:20:44 +0100 Subject: [PATCH] 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. --- supervisor/api/backups.py | 30 ++++++++++++++++++++++++------ supervisor/backups/manager.py | 5 +++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/supervisor/api/backups.py b/supervisor/api/backups.py index b37671bd9..9c601feef 100644 --- a/supervisor/api/backups.py +++ b/supervisor/api/backups.py @@ -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) diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index 7a655af53..308cdd39a 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -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."""