Преглед изворни кода

fix(windows): _local_zone falls back to stdlib utc when zoneinfo DB is missing

  The Windows installer's embedded Python doesn't carry an IANA tz
  database, and the stdlib zoneinfo has no system DB to read on Windows.
  ZoneInfo("UTC") raises ZoneInfoNotFoundError on those installs, and
  the new /api/local-backup/status endpoint 500s on the resulting
  uncaught exception. Surfaced via a Windows traceback from a user's log:

    File "...\backend\app\services\local_backup.py", line 32, in _local_zone
      return ZoneInfo("UTC")
    zoneinfo._common.ZoneInfoNotFoundError: 'No time zone found with key UTC'

  _local_zone()'s try/except only covered the TZ-env branch — both
  fallbacks unconditionally called ZoneInfo("UTC") and re-raised.

  Fix (two parts):

  1. services/local_backup.py — return type widened from ZoneInfo to
     tzinfo, the UTC fallback is wrapped in its own try, and the
     last-resort fallback returns datetime.timezone.utc (stdlib, no
     IANA DB needed). str(timezone.utc) == "UTC" so the response shape
     on /api/local-backup/status is unchanged. The astimezone call in
     _calculate_next_run accepts any tzinfo — no other call sites
     affected.

  2. requirements.txt — pin tzdata>=2024.1; sys_platform == "win32" so
     the next Windows installer build ships the IANA DB, and any non-
     UTC TZ value (e.g. Europe/Berlin) resolves correctly. The stdlib
     fallback can only ever give UTC. Linux/macOS unaffected by the
     platform marker — they already have the system tz database.
maziggy пре 2 месеци
родитељ
комит
f15e54c383
4 измењених фајлова са 45 додато и 7 уклоњено
  1. 1 0
      CHANGELOG.md
  2. 17 7
      backend/app/services/local_backup.py
  3. 19 0
      backend/tests/unit/test_local_backup.py
  4. 8 0
      requirements.txt

Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
CHANGELOG.md


+ 17 - 7
backend/app/services/local_backup.py

@@ -7,7 +7,7 @@ on a configurable schedule with retention management.
 import asyncio
 import logging
 import os
-from datetime import datetime, timedelta, timezone
+from datetime import datetime, timedelta, timezone, tzinfo
 from pathlib import Path
 from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
 
@@ -20,21 +20,31 @@ from backend.app.models.settings import Settings
 logger = logging.getLogger(__name__)
 
 
-def _local_zone() -> ZoneInfo:
+def _local_zone() -> tzinfo:
     """Resolve the local timezone for scheduled-backup HH:MM interpretation.
 
     Uses the container's ``TZ`` env var (the same value the support package
     surfaces); falls back to UTC when unset or unrecognised so a missing TZ
     keeps the legacy behaviour rather than crashing. See #1602 follow-up.
+
+    On Windows the embedded Python in our installer doesn't carry an IANA
+    tz database, so ``ZoneInfo(...)`` — including ``ZoneInfo("UTC")`` —
+    raises ``ZoneInfoNotFoundError`` unless the ``tzdata`` PyPI package is
+    installed. requirements.txt now pins ``tzdata`` on win32, but to keep
+    this resilient on installs that haven't refreshed deps we fall through
+    to the stdlib ``datetime.timezone.utc`` as a last resort; it satisfies
+    every ``astimezone`` / ``str()`` call site without needing the IANA DB.
     """
     tz_name = os.environ.get("TZ", "").strip()
-    if not tz_name:
-        return ZoneInfo("UTC")
+    if tz_name:
+        try:
+            return ZoneInfo(tz_name)
+        except ZoneInfoNotFoundError:
+            logger.warning("Unrecognised TZ env value %r, scheduling in UTC", tz_name)
     try:
-        return ZoneInfo(tz_name)
-    except ZoneInfoNotFoundError:
-        logger.warning("Unrecognised TZ env value %r, scheduling in UTC", tz_name)
         return ZoneInfo("UTC")
+    except ZoneInfoNotFoundError:
+        return timezone.utc
 
 
 SCHEDULE_INTERVALS = {

+ 19 - 0
backend/tests/unit/test_local_backup.py

@@ -137,6 +137,25 @@ class TestCalculateNextRun:
             result = service._calculate_next_run("daily", "21:00")
         assert result == datetime(2026, 6, 15, 21, 0, 0, tzinfo=timezone.utc)
 
+    def test_zoneinfo_completely_unavailable_falls_back_to_stdlib_utc(self, monkeypatch):
+        """Windows installer ships an embedded Python without the IANA tz DB
+        (no system tzdata, no ``tzdata`` PyPI package). Even ``ZoneInfo("UTC")``
+        raises ``ZoneInfoNotFoundError`` then, and /api/local-backup/status
+        500s. The fallback must catch that and return ``datetime.timezone.utc``
+        so scheduling still works without the DB.
+        """
+        from zoneinfo import ZoneInfoNotFoundError
+
+        from backend.app.services import local_backup as lb_module
+
+        monkeypatch.delenv("TZ", raising=False)
+
+        def _always_missing(_key):
+            raise ZoneInfoNotFoundError("no tz database on this platform")
+
+        monkeypatch.setattr(lb_module, "ZoneInfo", _always_missing)
+        assert lb_module._local_zone() is timezone.utc
+
     def test_dst_spring_forward_gap_does_not_crash(self, monkeypatch):
         """Europe/Berlin spring-forward 2026-03-29 jumps 02:00 → 03:00 local;
         02:30 wall-clock does not exist. ``replace(hour=2, minute=30)`` should

+ 8 - 0
requirements.txt

@@ -65,6 +65,14 @@ fast-simplification>=0.1.0
 # System monitoring
 psutil>=6.0.0
 
+# IANA tz database for Windows. The stdlib ``zoneinfo`` module reads the
+# system tz database on Linux/macOS, but Windows has none — and the
+# embedded Python in our Windows installer doesn't carry one either, so
+# even ``ZoneInfo("UTC")`` raises ``ZoneInfoNotFoundError`` and any
+# endpoint that resolves a tz (e.g. /api/local-backup/status) 500s.
+# ``tzdata`` is the official PyPI package that fills the gap.
+tzdata>=2024.1; sys_platform == "win32"
+
 # Authentication
 PyJWT>=2.13.0
 passlib[bcrypt]>=1.7.4

Неке датотеке нису приказане због велике количине промена