Skip to content

Commit 5994e47

Browse files
sabeechenclaude
andcommitted
Point the restore link at HA's new backup page and lock in credential redaction
The restore link sent users to the hassio/backups panel, which Home Assistant removed in 2025.1 when backups moved to Settings > System > Backups (issue #1151). The link now picks the right page based on the Home Assistant version, assuming a current version when unknown. Credential redaction in the UI server's error logging (issue #1001) was already implemented; this adds test coverage for the original report's repro path and for redaction of serialized credentials on the OAuth return route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 00fede9 commit 5994e47

5 files changed

Lines changed: 58 additions & 4 deletions

File tree

hassio-google-drive-backup/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## v0.113.0 [unreleased]
22

3+
- The web UI's "restore" link now points at Settings > System > Backups on Home Assistant 2025.1 and later, where the backup page moved. Older versions still get the old link.
4+
- User-supplied Google API credentials (and serialized tokens) are redacted from the addon's logs when a request fails.
35
- The addon now recognizes backups created by Home Assistant's own automatic backup schedule (added in Home Assistant 2025.1) and, on new installs, ignores them by default so it never deletes backups that Home Assistant's retention settings are managing. Existing installs keep the old behavior unless the new `ignore_automatic_backups` option is enabled, since some users may rely on those backups being uploaded to Google Drive.
46
- Clicking "Never Delete" on an ignored backup now also un-ignores it, so you no longer have to click "Don't Ignore" first and race the next sync's deletions to keep an ignored backup forever.
57
- Updated the addon to Python 3.14 with pinned, reproducible dependencies.

hassio-google-drive-backup/backup/ha/hasource.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,17 @@ def getHomeAssistantUrl(self):
482482
return ""
483483
return self._haUrl()
484484

485+
def getHomeAssistantVersion(self):
486+
if self.ha_info is None:
487+
return None
488+
version = self.ha_info.get("version", None)
489+
if version is None:
490+
return None
491+
try:
492+
return Version.parse(version)
493+
except (ValueError, TypeError):
494+
return None
495+
485496
def _haUrl(self):
486497
if self._info.ha_ssl:
487498
protocol = "https://"

hassio-google-drive-backup/backup/ui/uiserver.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
MIME_JSON = "application/json"
4040
VERSION_CREATION_TRACKING = Version(0, 104, 0)
4141

42+
# Home Assistant 2025.1 moved backups from the old "hassio/backups" panel to Settings > System > Backups.
43+
VERSION_HA_BACKUP_PANEL_MOVED = Version(2025, 1)
44+
4245

4346
@singleton
4447
class UiServer(Trigger, Startable):
@@ -103,7 +106,12 @@ async def buildStatusInfo(self):
103106
for backup in backups:
104107
status['backups'].append(self.getBackupDetails(backup))
105108
status['ha_url_base'] = self._ha_source.getHomeAssistantUrl()
106-
status['restore_backup_path'] = "hassio/backups"
109+
ha_version = self._ha_source.getHomeAssistantVersion()
110+
if ha_version is not None and ha_version < VERSION_HA_BACKUP_PANEL_MOVED:
111+
status['restore_backup_path'] = "hassio/backups"
112+
else:
113+
# When the version is unknown, assume a current Home Assistant.
114+
status['restore_backup_path'] = "config/backup/backups"
107115
status['ask_error_reports'] = not self.config.isExplicit(
108116
Setting.SEND_ERROR_REPORTS)
109117
status['warn_ingress_upgrade'] = False

hassio-google-drive-backup/dev/simulated_supervisor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(self, config: Config, ports: Ports, time: Time):
5555
self._password = "pass"
5656
self._addons = all_addons.copy()
5757
self._super_version = Version(2023, 7)
58+
self._core_version = Version(2026, 7)
5859
self._mounts = {
5960
'default_backup_mount': None,
6061
'mounts': [
@@ -275,8 +276,8 @@ async def _coreInfo(self, request: Request):
275276
await self._verifyHeader(request)
276277
return self._formatDataResponse(
277278
{
278-
"version": "1.3.3.7",
279-
"last_version": "1.3.3.8",
279+
"version": str(self._core_version),
280+
"last_version": str(self._core_version),
280281
"machine": "VS Dev",
281282
"ip_address": "127.0.0.1",
282283
"arch": "x86",

hassio-google-drive-backup/tests/test_uiserver.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
from aiohttp.client import ClientSession
1414

1515
from backup.file import File
16+
from backup.logger import getHistory
1617
from backup.util import AsyncHttpGetter, GlobalInfo, DataCache, UpgradeFlags
1718
from backup.ui import UiServer, Restarter
18-
from backup.config import Config, Setting, CreateOptions
19+
from backup.config import Config, Setting, CreateOptions, Version
1920
from backup.const import (ERROR_CREDS_EXPIRED, ERROR_EXISTING_FOLDER,
21+
ERROR_GOOGLE_CRED_PROCESS,
2022
ERROR_MULTIPLE_DELETES, ERROR_NO_BACKUP,
2123
SOURCE_GOOGLE_DRIVE, SOURCE_HA)
2224
from backup.creds import Creds
@@ -29,6 +31,7 @@
2931
from .helpers import compareStreams
3032
from yarl import URL
3133
from dev.ports import Ports
34+
from dev.request_interceptor import RequestInterceptor
3235
from dev.simulated_supervisor import SimulatedSupervisor
3336
from dev.simulationserver import SimulationServer
3437
from dev.simulated_google import SimulatedGoogle
@@ -1200,3 +1203,32 @@ async def test_oob_warning(reader: ReaderHelper, ui_server: UiServer, config: Co
12001203
@pytest.mark.asyncio
12011204
async def test_url_sanitize(ui_server: UiServer):
12021205
assert ui_server._sanitize(URL("http://localhost/test?client_id=im_a_secret&client_secret=im_a_secret&ignore=shown")) == URL("http://localhost/test?client_id=redacted&client_secret=redacted&ignore=shown")
1206+
assert ui_server._sanitize(URL("http://localhost/token?creds=im_a_serialized_refresh_token&host=shown")) == URL("http://localhost/token?creds=redacted&host=shown")
1207+
1208+
1209+
@pytest.mark.asyncio
1210+
async def test_manualauth_error_redacts_credentials(reader: ReaderHelper, interceptor: RequestInterceptor):
1211+
"""Credentials passed to manualauth must never reach the logs when a request fails (issue #1001)"""
1212+
interceptor.setError("/device/code", status=401)
1213+
await reader.assertError("manualauth?client_id=super_secret_id&client_secret=super_secret_value", error_type=ERROR_GOOGLE_CRED_PROCESS)
1214+
logs = "\n".join(line for _, line in getHistory(0, False))
1215+
assert "Error serving" in logs
1216+
assert "super_secret_id" not in logs
1217+
assert "super_secret_value" not in logs
1218+
assert "client_secret=redacted" in logs
1219+
1220+
1221+
@pytest.mark.asyncio
1222+
async def test_restore_link_current_ha(reader: ReaderHelper):
1223+
"""Home Assistant 2025.1 and later hosts backups at Settings > System > Backups (issue #1151)"""
1224+
status = await reader.getjson("getstatus")
1225+
assert status['restore_backup_path'] == "config/backup/backups"
1226+
1227+
1228+
@pytest.mark.asyncio
1229+
async def test_restore_link_old_ha(reader: ReaderHelper, supervisor: SimulatedSupervisor, ha: HaSource):
1230+
"""Home Assistant older than 2025.1 uses the old hassio/backups panel"""
1231+
supervisor._core_version = Version(2024, 12)
1232+
await ha.refresh()
1233+
status = await reader.getjson("getstatus")
1234+
assert status['restore_backup_path'] == "hassio/backups"

0 commit comments

Comments
 (0)