From 53f0fd339fabe5151545e406ff9b95681db9409e Mon Sep 17 00:00:00 2001 From: SMo Date: Tue, 10 Feb 2026 19:58:54 +0100 Subject: [PATCH 1/5] Add backup retention enforcement for Samba backups Implement a new function to enforce backup retention by deleting older backups on the SMB share when the number exceeds the specified limit. The maximum number of backups is now configurable via the SambaBackupCloudConfiguration class. This change ensures that only the most recent backups are retained, improving storage management. Additionally, integrate the retention enforcement into the upload_backup function, handling potential errors during the cleanup process gracefully. --- .../backup_clouds/samba/backup_cloud.py | 73 ++++++++++++++++++- .../modules/backup_clouds/samba/config.py | 5 +- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/modules/backup_clouds/samba/backup_cloud.py b/packages/modules/backup_clouds/samba/backup_cloud.py index 49b1145343..58faac2012 100644 --- a/packages/modules/backup_clouds/samba/backup_cloud.py +++ b/packages/modules/backup_clouds/samba/backup_cloud.py @@ -28,6 +28,59 @@ def is_port_open(host: str, port: int): s.close() +def _enforce_retention(conn, config: SambaBackupCloudConfiguration, backup_filename: str) -> None: + """ + Löscht alte Backups auf dem SMB-Share, wenn mehr als max_backups vorhanden sind. + Es werden nur Dateien berücksichtigt, deren Dateiname mit dem Basisnamen der aktuellen + Backup-Datei beginnt (alles vor dem ersten Punkt). + """ + max_backups = getattr(config, "max_backups", None) + if not max_backups or max_backups <= 0: + return + + # Verzeichnis ermitteln, in dem die Backups liegen + smb_path = config.smb_path or "/" + smb_path = smb_path.rstrip("/") + if smb_path == "": + dir_path = "/" + else: + dir_path = smb_path + + # Basispräfix der aktuellen Backup-Datei (z.B. "openwb-backup-2026-02-10" aus "openwb-backup-2026-02-10.tar.gz") + base_name = os.path.basename(backup_filename) + base_prefix = base_name.split(".")[0] + + # Alle Einträge im Backup-Verzeichnis holen + entries = conn.listPath(config.smb_share, dir_path) + + # Nur relevante Backup-Dateien herausfiltern + backup_files = [ + e for e in entries + if not e.isDirectory + and e.filename not in (".", "..") + and e.filename.startswith(base_prefix) + ] + + if len(backup_files) <= max_backups: + return + + # Nach Änderungszeit sortieren (neueste zuerst) + backup_files.sort(key=lambda e: e.last_write_time, reverse=True) + + # Ältere Backups über dem Limit löschen + for old_entry in backup_files[max_backups:]: + if dir_path in ("", "/"): + delete_path = f"/{old_entry.filename}" + else: + delete_path = f"{dir_path.rstrip('/')}/{old_entry.filename}" + try: + log.info("Lösche altes Samba-Backup: //%s/%s%s", config.smb_server, config.smb_share, delete_path) + conn.deleteFiles(config.smb_share, delete_path) + except Exception as error: + # Fehler beim Aufräumen sollen das eigentliche Backup nicht fehlschlagen lassen + log.error("Fehler beim Löschen alter Samba-Backups (%s): %s", delete_path, str(error).split("\n")[0]) + + def upload_backup(config: SambaBackupCloudConfiguration, backup_filename: str, backup_file: bytes) -> None: SMB_PORT_445 = 445 SMB_PORT_139 = 139 @@ -57,9 +110,19 @@ def upload_backup(config: SambaBackupCloudConfiguration, backup_filename: str, b conn.storeFile(config.smb_share, full_file_path, io.BytesIO(backup_file)) + try: + _enforce_retention(conn, config, backup_filename) + except Exception as error: + log.error( + "Fehler bei der Bereinigung alter Samba-Backups (Port 445): %s", + str(error).split("\n")[0], + ) + return except Exception as error: - raise Exception("Freigabe oder Unterordner existiert möglicherweise nicht. "+str(error).split('\n')[0]) + raise Exception( + "Freigabe oder Unterordner existiert möglicherweise nicht. " + str(error).split("\n")[0] + ) finally: conn.close() else: @@ -88,6 +151,14 @@ def upload_backup(config: SambaBackupCloudConfiguration, backup_filename: str, b log.info(f"Backup nach //{config.smb_server}/{config.smb_share}/{full_file_path}") conn.storeFile(config.smb_share, full_file_path, io.BytesIO(backup_file)) + + try: + _enforce_retention(conn, config, backup_filename) + except Exception as error: + log.error( + "Fehler bei der Bereinigung alter Samba-Backups (Port 139): %s", + str(error).split("\n")[0], + ) except Exception as error: raise Exception( "Möglicherweise ist die Freigabe oder ein Unterordner nicht vorhanden." diff --git a/packages/modules/backup_clouds/samba/config.py b/packages/modules/backup_clouds/samba/config.py index f8f5e483fc..91e598e6fd 100644 --- a/packages/modules/backup_clouds/samba/config.py +++ b/packages/modules/backup_clouds/samba/config.py @@ -7,12 +7,15 @@ def __init__(self, smb_server: Optional[str] = None, smb_share: Optional[str] = None, smb_user: Optional[str] = None, - smb_password: Optional[str] = None): + smb_password: Optional[str] = None, + max_backups: Optional[int] = None): self.smb_path = smb_path self.smb_server = smb_server self.smb_share = smb_share self.smb_user = smb_user self.smb_password = smb_password + # None oder <= 0 bedeutet: keine automatische Löschung alter Backups + self.max_backups = max_backups class SambaBackupCloud: From d759e6345d84490a084e5c56a86fa30938455ce6 Mon Sep 17 00:00:00 2001 From: SMo Date: Tue, 10 Feb 2026 20:13:48 +0100 Subject: [PATCH 2/5] Add backup retention and listing functionality for Nextcloud backups Enhance the Nextcloud backup module by introducing functions to list existing backups and enforce retention policies. The new `_list_backups` function retrieves backups based on a naming prefix, while `_enforce_retention` ensures that only a specified number of recent backups are retained. The `NextcloudBackupCloudConfiguration` class is updated to include a `max_backups` parameter for configuration. These changes improve backup management and storage efficiency. --- .../backup_clouds/nextcloud/backup_cloud.py | 124 +++++++++++++++++- .../modules/backup_clouds/nextcloud/config.py | 8 +- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/packages/modules/backup_clouds/nextcloud/backup_cloud.py b/packages/modules/backup_clouds/nextcloud/backup_cloud.py index c059190f2f..cdb5e55589 100644 --- a/packages/modules/backup_clouds/nextcloud/backup_cloud.py +++ b/packages/modules/backup_clouds/nextcloud/backup_cloud.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import logging import re +from typing import List from modules.backup_clouds.nextcloud.config import NextcloudBackupCloud, NextcloudBackupCloudConfiguration from modules.common import req @@ -9,26 +10,141 @@ log = logging.getLogger(__name__) -def upload_backup(config: NextcloudBackupCloudConfiguration, backup_filename: str, backup_file: bytes) -> None: +def _parse_nextcloud_base_url_and_user(config: NextcloudBackupCloudConfiguration, backup_filename: str): + """ + Liefert Basis-URL (ohne /public.php/webdav/...) und Benutzer (Token oder User). + Zusätzlich wird der WebDAV-Pfad zum Backup-Verzeichnis zurückgegeben. + """ if config.user is None: url_match = re.fullmatch(r'(http[s]?):\/\/([\S^/]+)\/(?:index.php\/)?s\/(.+)', config.ip_address) if not url_match: - raise ValueError(f"URL '{config.ip_address}' hat nicht die erwartete Form " - "'https://server/index.php/s/user_token' oder 'https://server/s/user_token'") + raise ValueError( + f"URL '{config.ip_address}' hat nicht die erwartete Form " + "'https://server/index.php/s/user_token' oder 'https://server/s/user_token'" + ) upload_url = f"{url_match[1]}://{url_match[2]}" user = url_match[url_match.lastindex] + base_path = "/public.php/webdav" else: upload_url = config.ip_address user = config.user + # Für Benutzer-Accounts wird üblicherweise /remote.php/dav/files// verwendet. + # Hier bleiben wir beim bisherigen Verhalten (/public.php/webdav/), + # d.h. base_path ist leer und backup_filename enthält ggf. Unterordner. + base_path = "" + + return upload_url, user, base_path + + +def _list_backups(config: NextcloudBackupCloudConfiguration, + backup_filename: str) -> List[str]: + """ + Listet alle vorhandenen Backupdateien, die zum gleichen Präfix gehören + (Pattern-Match am Dateinamen) und gibt eine nach Dateinamen sortierte Liste zurück. + """ + max_backups = getattr(config, "max_backups", None) + if not max_backups or max_backups <= 0: + return [] + + upload_url, user, base_path = _parse_nextcloud_base_url_and_user(config, backup_filename) + + # Präfix aus aktuellem Backup ableiten (alles vor dem ersten Punkt) + # Beispiel: openwb-backup-2026-02-10.tar.gz -> openwb-backup-2026-02-10 + base_name = backup_filename.split("/")[-1] + base_prefix = base_name.split(".")[0] + + # WebDAV PROPFIND, um Dateiliste zu bekommen + list_path = f"{base_path}/" + response = req.get_http_session().request( + "PROPFIND", + f"{upload_url}{list_path}", + headers={ + "Depth": "1", + "Content-Type": "text/xml", + }, + data=""" + + + + +""", + auth=(user, "" if config.password is None else config.password), + timeout=60, + ) + + if not response.ok: + log.warning("Nextcloud PROPFIND für Backup-Liste fehlgeschlagen: %s %s", + response.status_code, response.reason) + return [] + + # Sehr einfache Auswertung: nach ... parsen + # und alle Einträge sammeln, die mit unserem Präfix beginnen. + names: List[str] = [] + for match in re.finditer(r"([^<]+)", response.text): + name = match.group(1) + if name.startswith(base_prefix): + names.append(name) + # Alphabetisch sortieren – entspricht der im Issue gewünschten Sortierung nach Dateinamen + names.sort() + return names + + +def _enforce_retention(config: NextcloudBackupCloudConfiguration, backup_filename: str) -> None: + """ + Löscht alte Nextcloud-Backups, so dass höchstens max_backups Dateien mit dem + gleichen Namenspräfix (Pattern-Match) übrig bleiben. Sortierung erfolgt nach + Dateinamen, es bleiben die letzten N erhalten. + """ + max_backups = getattr(config, "max_backups", None) + if not max_backups or max_backups <= 0: + return + + upload_url, user, base_path = _parse_nextcloud_base_url_and_user(config, backup_filename) + all_backups = _list_backups(config, backup_filename) + if len(all_backups) <= max_backups: + return + + # Alle außer den letzten max_backups löschen + to_delete = all_backups[:-max_backups] + + for name in to_delete: + delete_path = f"{base_path}/{name}" if base_path else name + try: + log.info("Lösche altes Nextcloud-Backup: %s", delete_path) + resp = req.get_http_session().delete( + f"{upload_url}{delete_path}", + headers={"X-Requested-With": "XMLHttpRequest"}, + auth=(user, "" if config.password is None else config.password), + timeout=60, + ) + if not resp.ok: + log.warning("Löschen des Nextcloud-Backups '%s' fehlgeschlagen: %s %s", + delete_path, resp.status_code, resp.reason) + except Exception as error: + log.error("Fehler beim Löschen alter Nextcloud-Backups (%s): %s", + delete_path, str(error).split("\n")[0]) + + +def upload_backup(config: NextcloudBackupCloudConfiguration, backup_filename: str, backup_file: bytes) -> None: + upload_url, user, base_path = _parse_nextcloud_base_url_and_user(config, backup_filename) + + # Backup-Datei hochladen req.get_http_session().put( - f'{upload_url}/public.php/webdav/{backup_filename}', + f"{upload_url}{base_path}/{backup_filename.lstrip('/')}", headers={'X-Requested-With': 'XMLHttpRequest', }, data=backup_file, auth=(user, '' if config.password is None else config.password), timeout=60 ) + # Aufbewahrung alter Backups erzwingen (wenn konfiguriert) + try: + _enforce_retention(config, backup_filename) + except Exception as error: + log.error("Fehler bei der Bereinigung alter Nextcloud-Backups: %s", + str(error).split("\n")[0]) + def create_backup_cloud(config: NextcloudBackupCloud): def updater(backup_filename: str, backup_file: bytes): diff --git a/packages/modules/backup_clouds/nextcloud/config.py b/packages/modules/backup_clouds/nextcloud/config.py index 6f19a8bd13..972c85ef60 100644 --- a/packages/modules/backup_clouds/nextcloud/config.py +++ b/packages/modules/backup_clouds/nextcloud/config.py @@ -2,10 +2,16 @@ class NextcloudBackupCloudConfiguration: - def __init__(self, ip_address: Optional[str] = None, user: Optional[str] = None, password: Optional[str] = None): + def __init__(self, + ip_address: Optional[str] = None, + user: Optional[str] = None, + password: Optional[str] = None, + max_backups: Optional[int] = None): self.ip_address = ip_address self.user = user self.password = password + # None oder <= 0 bedeutet: keine automatische Löschung alter Backups + self.max_backups = max_backups class NextcloudBackupCloud: From dbf7a21b185426870482a469e94b024c6a8f3d84 Mon Sep 17 00:00:00 2001 From: SMo Date: Fri, 20 Mar 2026 15:54:20 +0100 Subject: [PATCH 3/5] Refine backup retention logic for Nextcloud and Samba Update the backup retention mechanisms in both Nextcloud and Samba modules to use suffix-based filtering for backup filenames. The changes ensure that only files ending with ".openwb-backup" or ".openwb-backup.gpg" are considered for retention, improving accuracy in backup management. Additionally, comments have been enhanced for clarity regarding the implementation details and expected filename patterns. --- .../backup_clouds/nextcloud/backup_cloud.py | 31 ++++++++++++------- .../backup_clouds/samba/backup_cloud.py | 26 ++++++++++------ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/packages/modules/backup_clouds/nextcloud/backup_cloud.py b/packages/modules/backup_clouds/nextcloud/backup_cloud.py index cdb5e55589..7e0c0937ac 100644 --- a/packages/modules/backup_clouds/nextcloud/backup_cloud.py +++ b/packages/modules/backup_clouds/nextcloud/backup_cloud.py @@ -28,10 +28,11 @@ def _parse_nextcloud_base_url_and_user(config: NextcloudBackupCloudConfiguration else: upload_url = config.ip_address user = config.user - # Für Benutzer-Accounts wird üblicherweise /remote.php/dav/files// verwendet. - # Hier bleiben wir beim bisherigen Verhalten (/public.php/webdav/), - # d.h. base_path ist leer und backup_filename enthält ggf. Unterordner. - base_path = "" + # Für Benutzer-Accounts ist normalerweise /remote.php/dav/files// üblich. + # In dieser Implementierung verwenden wir aber bewusst weiterhin den + # öffentlichen WebDAV-Pfad wie beim vorherigen Verhalten: + # /public.php/webdav/ + base_path = "/public.php/webdav" return upload_url, user, base_path @@ -39,7 +40,7 @@ def _parse_nextcloud_base_url_and_user(config: NextcloudBackupCloudConfiguration def _list_backups(config: NextcloudBackupCloudConfiguration, backup_filename: str) -> List[str]: """ - Listet alle vorhandenen Backupdateien, die zum gleichen Präfix gehören + Listet alle vorhandenen Backupdateien, die zum gleichen OpenWB-Suffix gehören (Pattern-Match am Dateinamen) und gibt eine nach Dateinamen sortierte Liste zurück. """ max_backups = getattr(config, "max_backups", None) @@ -48,10 +49,18 @@ def _list_backups(config: NextcloudBackupCloudConfiguration, upload_url, user, base_path = _parse_nextcloud_base_url_and_user(config, backup_filename) - # Präfix aus aktuellem Backup ableiten (alles vor dem ersten Punkt) - # Beispiel: openwb-backup-2026-02-10.tar.gz -> openwb-backup-2026-02-10 + # Robust gruppieren: OpenWB-Backups enden entweder auf ".openwb-backup" + # oder auf ".openwb-backup.gpg". Der Teil vor dem ersten Punkt kann + # timestamps-/versionspezifisch sein und darf daher nicht zum Gruppieren + # verwendet werden. base_name = backup_filename.split("/")[-1] - base_prefix = base_name.split(".")[0] + if base_name.endswith(".openwb-backup.gpg"): + required_suffix = ".openwb-backup.gpg" + elif base_name.endswith(".openwb-backup"): + required_suffix = ".openwb-backup" + else: + log.warning("Nextcloud Retention: Unerwartetes Backup-Dateimuster: %s", base_name) + return [] # WebDAV PROPFIND, um Dateiliste zu bekommen list_path = f"{base_path}/" @@ -78,11 +87,11 @@ def _list_backups(config: NextcloudBackupCloudConfiguration, return [] # Sehr einfache Auswertung: nach ... parsen - # und alle Einträge sammeln, die mit unserem Präfix beginnen. + # und alle Einträge sammeln, die auf unser Suffix enden. names: List[str] = [] for match in re.finditer(r"([^<]+)", response.text): name = match.group(1) - if name.startswith(base_prefix): + if name.endswith(required_suffix): names.append(name) # Alphabetisch sortieren – entspricht der im Issue gewünschten Sortierung nach Dateinamen @@ -93,7 +102,7 @@ def _list_backups(config: NextcloudBackupCloudConfiguration, def _enforce_retention(config: NextcloudBackupCloudConfiguration, backup_filename: str) -> None: """ Löscht alte Nextcloud-Backups, so dass höchstens max_backups Dateien mit dem - gleichen Namenspräfix (Pattern-Match) übrig bleiben. Sortierung erfolgt nach + gleichen OpenWB-Suffix (Pattern-Match) übrig bleiben. Sortierung erfolgt nach Dateinamen, es bleiben die letzten N erhalten. """ max_backups = getattr(config, "max_backups", None) diff --git a/packages/modules/backup_clouds/samba/backup_cloud.py b/packages/modules/backup_clouds/samba/backup_cloud.py index 58faac2012..9ee9a54a1f 100644 --- a/packages/modules/backup_clouds/samba/backup_cloud.py +++ b/packages/modules/backup_clouds/samba/backup_cloud.py @@ -31,8 +31,9 @@ def is_port_open(host: str, port: int): def _enforce_retention(conn, config: SambaBackupCloudConfiguration, backup_filename: str) -> None: """ Löscht alte Backups auf dem SMB-Share, wenn mehr als max_backups vorhanden sind. - Es werden nur Dateien berücksichtigt, deren Dateiname mit dem Basisnamen der aktuellen - Backup-Datei beginnt (alles vor dem ersten Punkt). + Es werden nur Dateien berücksichtigt, die auf ".openwb-backup" bzw. + ".openwb-backup.gpg" enden (stabiler Filter; der Teil vor der Endung ist + timestamps-/versionspezifisch und darf nicht zum Gruppieren verwendet werden). """ max_backups = getattr(config, "max_backups", None) if not max_backups or max_backups <= 0: @@ -46,9 +47,16 @@ def _enforce_retention(conn, config: SambaBackupCloudConfiguration, backup_filen else: dir_path = smb_path - # Basispräfix der aktuellen Backup-Datei (z.B. "openwb-backup-2026-02-10" aus "openwb-backup-2026-02-10.tar.gz") base_name = os.path.basename(backup_filename) - base_prefix = base_name.split(".")[0] + if base_name.endswith(".openwb-backup.gpg"): + required_suffix = ".openwb-backup.gpg" + elif base_name.endswith(".openwb-backup"): + required_suffix = ".openwb-backup" + else: + # Wenn das Dateimuster nicht passt, vermeiden wir versehentliches + # Löschen fremder Dateien auf dem Share. + log.warning("Samba Retention: Unerwartetes Backup-Dateimuster: %s", base_name) + return [] # Alle Einträge im Backup-Verzeichnis holen entries = conn.listPath(config.smb_share, dir_path) @@ -58,17 +66,17 @@ def _enforce_retention(conn, config: SambaBackupCloudConfiguration, backup_filen e for e in entries if not e.isDirectory and e.filename not in (".", "..") - and e.filename.startswith(base_prefix) + and e.filename.endswith(required_suffix) ] if len(backup_files) <= max_backups: return - # Nach Änderungszeit sortieren (neueste zuerst) - backup_files.sort(key=lambda e: e.last_write_time, reverse=True) + # Nach Dateiname sortieren (Issue #2020: Dateiname enthält die Reihenfolge) + backup_files.sort(key=lambda e: e.filename) - # Ältere Backups über dem Limit löschen - for old_entry in backup_files[max_backups:]: + # Ältere Backups über dem Limit löschen (alles bis auf die letzten max_backups) + for old_entry in backup_files[:-max_backups]: if dir_path in ("", "/"): delete_path = f"/{old_entry.filename}" else: From a0a68e3909218a0d9e94df7d4fef523c8c06f155 Mon Sep 17 00:00:00 2001 From: SMo Date: Mon, 23 Mar 2026 11:41:02 +0100 Subject: [PATCH 4/5] =?UTF-8?q?Dieses=20PR=20behebt=20einen=20Fehler=20in?= =?UTF-8?q?=20der=20Retention-Logik=20f=C3=BCr=20Cloud-Backups:=20Die=20Au?= =?UTF-8?q?swahl=20der=20zu=20l=C3=B6schenden=20Backup-Generationen=20erfo?= =?UTF-8?q?lgt=20jetzt=20anhand=20eines=20stabilen=20Suffix-Musters=20(.op?= =?UTF-8?q?enwb-backup=20/=20.openwb-backup.gpg)=20statt=20eines=20instabi?= =?UTF-8?q?len=20Prefixes=20(split('.')).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/helpermodules/update_config.py | 29 ++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index f79bff292e..d33003e839 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 113 + DATASTORE_VERSION = 114 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2958,3 +2958,30 @@ def upgrade(topic: str, payload) -> None: self._loop_all_received_topics(upgrade) self._append_datastore_version(113) + + def upgrade_datastore_114(self) -> None: + """ + Ensure new backup cloud configuration field `max_backups` is present. + + For older datastores the field may be missing; normalizing to `0` + guarantees the semantics "0 = keine automatische Löschung" and + prevents null/undefined being written back from the UI. + """ + + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search(r"^openWB/system/backup_cloud/config$", topic) is None: + return None + + configuration_payload = decode_payload(payload) + cloud_type = configuration_payload.get("type") + if cloud_type not in ("nextcloud", "samba"): + return None + + configuration_payload.setdefault("configuration", {}) + if configuration_payload["configuration"].get("max_backups") is None: + configuration_payload["configuration"]["max_backups"] = 0 + + return {topic: configuration_payload} + + self._loop_all_received_topics(upgrade) + self._append_datastore_version(114) From ef484365179359e45ee2afe28b5a7cc8c9490524 Mon Sep 17 00:00:00 2001 From: SMo Date: Mon, 23 Mar 2026 12:20:06 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Danke=20f=C3=BCr=20die=20R=C3=BCckmeldung.?= =?UTF-8?q?=20Backend-seitig=20ist=20max=5Fbackups=20jetzt=20sowohl=20im?= =?UTF-8?q?=20Upgrade-Pfad=20als=20auch=20im=20normalen=20update=5Fconfig-?= =?UTF-8?q?Pfad=20f=C3=BCr=20nextcloud=20und=20samba=20auf=20null/None=20?= =?UTF-8?q?=3D>=200=20abgesichert.=20Frontend-seitig=20behalten=20wir=20zu?= =?UTF-8?q?s=C3=A4tzlich=20die=20Absicherung=20mit=20=3F=3F=200=20und=20sc?= =?UTF-8?q?hreiben=20bei=20leerem=20Feld=20ebenfalls=200=20zur=C3=BCck,=20?= =?UTF-8?q?damit=20der=20Wert=20auch=20in=20der=20UI=20jederzeit=20konsist?= =?UTF-8?q?ent=20bleibt=20(Defense-in-depth).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/helpermodules/update_config.py | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index d33003e839..178f58bb74 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -710,6 +710,9 @@ def update(self): try: # erst breaking changes auflösen, sonst sind alte Topics schon gelöscht self.__solve_breaking_changes() + # Normalisiere neue Felder auch im normalen Update-Pfad, nicht nur in + # upgrade_datastore_* (UI kann aktuell bei "Feld geleert" null übermitteln). + self.__normalize_backup_cloud_max_backups() self.__remove_outdated_topics() self._remove_invalid_topics() self.__pub_missing_defaults() @@ -810,6 +813,31 @@ def __solve_breaking_changes(self) -> None: pub_system_message( {}, "Fehler bei der Aktualisierung der Konfiguration des Brokers.", MessageType.ERROR) + def __normalize_backup_cloud_max_backups(self) -> None: + """ + Serverseitige Normalisierung für `configuration.max_backups` im + normalen Update-Pfad. + + Semantik: + - `0` => automatische Löschung deaktiviert + """ + for topic, payload in list(self.all_received_topics.items()): + if topic != "openWB/system/backup_cloud/config": + continue + + configuration_payload = decode_payload(payload) + if not isinstance(configuration_payload, dict): + continue + + cloud_type = configuration_payload.get("type") + if cloud_type not in ("nextcloud", "samba"): + continue + + configuration_payload.setdefault("configuration", {}) + if configuration_payload["configuration"].get("max_backups") is None: + configuration_payload["configuration"]["max_backups"] = 0 + self.__update_topic(topic, configuration_payload) + def _loop_all_received_topics(self, callback) -> None: modified_topics = {} for topic, payload in self.all_received_topics.items():