feat(config): TZ + NTP_SERVER in .env mit sinnvollen Defaults
- .env / .env.example: TZ=Europe/Berlin und NTP_SERVER=ptbtime1.ptb.de (offizielle deutsche Zeitreferenz, hohe Verfuegbarkeit) - app/__init__.py setzt prozessweite Zeitzone frueh via os.environ+tzset - Leichtgewichtiger SNTP-Client (pure socket, keine deps) prueft den Uhr-Offset beim Start im Hintergrund-Thread und warnt bei Abweichung >5s - Dockerfile installiert tzdata und ENV TZ=Europe/Berlin als Fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ba3e619963
commit
dca064427e
12
.env.example
12
.env.example
|
|
@ -31,6 +31,18 @@ FRONTEND_URL=https://cloud.example.com
|
|||
# Max Upload-Groesse in MB
|
||||
MAX_UPLOAD_SIZE_MB=500
|
||||
|
||||
# Zeitzone (prozessweit) - z.B. Europe/Berlin, Europe/Vienna, UTC
|
||||
# Wirkt auf datetime.now(), strftime %Z und Kalender/Task-Zeitstempel.
|
||||
TZ=Europe/Berlin
|
||||
|
||||
# NTP-Server zum Pruefen der Uhrzeit beim Start (nicht-invasiver Offset-Check
|
||||
# - im Container kann die Systemuhr nicht gesetzt werden; bei Abweichung >5s
|
||||
# erscheint eine Warnung im Log, dann bitte die Host-Uhr synchronisieren).
|
||||
# Leerlassen um den Check zu deaktivieren.
|
||||
# Default: Physikalisch-Technische Bundesanstalt (offizielle deutsche Zeit).
|
||||
# Alternativen: ptbtime2.ptb.de, ptbtime3.ptb.de, de.pool.ntp.org, time.cloudflare.com
|
||||
NTP_SERVER=ptbtime1.ptb.de
|
||||
|
||||
# OnlyOffice Document Server (optional)
|
||||
# Eigene Subdomain mit HTTPS, z.B. https://office.example.com
|
||||
# JWT wird automatisch vom JWT_SECRET_KEY oben verwendet
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ WORKDIR /app
|
|||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
|
|
@ -30,6 +31,7 @@ RUN mkdir -p /app/data/files
|
|||
|
||||
# Environment
|
||||
ENV FLASK_ENV=production
|
||||
ENV TZ=Europe/Berlin
|
||||
ENV DATABASE_PATH=/app/data/minicloud.db
|
||||
ENV UPLOAD_PATH=/app/data/files
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Response, redirect, send_from_directory
|
||||
|
|
@ -8,6 +9,20 @@ from app.config import Config
|
|||
from app.extensions import db, bcrypt, migrate
|
||||
|
||||
|
||||
def _configure_timezone(tz_name: str) -> None:
|
||||
"""Prozess-Zeitzone setzen, sodass datetime.now(), strftime %Z etc.
|
||||
die konfigurierte TZ verwenden. Sichere no-op wenn tzdata fehlt."""
|
||||
if not tz_name:
|
||||
return
|
||||
os.environ['TZ'] = tz_name
|
||||
tzset = getattr(time, 'tzset', None)
|
||||
if tzset:
|
||||
try:
|
||||
tzset()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _auto_migrate(db):
|
||||
"""Add missing columns to existing tables by comparing model definitions
|
||||
with actual database schema. This handles the case where new columns are
|
||||
|
|
@ -61,6 +76,9 @@ def _auto_migrate(db):
|
|||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
# Zeitzone moeglichst frueh setzen - vor allen datetime.now()-Aufrufen
|
||||
_configure_timezone(getattr(config_class, 'TIMEZONE', None) or os.environ.get('TZ'))
|
||||
|
||||
# Check if static frontend build exists (Docker production mode)
|
||||
static_dir = Path(__file__).resolve().parent.parent / 'static'
|
||||
if static_dir.exists():
|
||||
|
|
@ -171,4 +189,15 @@ def create_app(config_class=Config):
|
|||
from app.services.backup_scheduler import start_backup_scheduler
|
||||
start_backup_scheduler(app)
|
||||
|
||||
# NTP-Offset gegen den konfigurierten Zeitserver pruefen (nicht fatal).
|
||||
ntp_server = app.config.get('NTP_SERVER') or ''
|
||||
if ntp_server.strip():
|
||||
import threading
|
||||
from app.services.ntp_check import check_and_log
|
||||
threading.Thread(
|
||||
target=check_and_log,
|
||||
args=(ntp_server.strip(), app.logger),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -40,3 +40,8 @@ class Config:
|
|||
|
||||
# CORS
|
||||
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')
|
||||
|
||||
# Zeitzone (prozessweit, wirkt nach time.tzset())
|
||||
TIMEZONE = os.environ.get('TZ', 'Europe/Berlin')
|
||||
# NTP-Server fuer Offset-Check beim Start. Leerstring deaktiviert den Check.
|
||||
NTP_SERVER = os.environ.get('NTP_SERVER', 'ptbtime1.ptb.de')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
"""Leichtgewichtiger SNTP-Client zum Pruefen des Zeit-Offsets.
|
||||
|
||||
Im Container koennen wir die Systemzeit nicht wirklich setzen (braucht
|
||||
CAP_SYS_TIME). Aber wir koennen den Offset ermitteln und loggen, damit
|
||||
der Admin weiss, ob der Host driftet. Fuer einen harten Sync muss auf
|
||||
dem Host selbst ein NTP-Daemon laufen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
_NTP_EPOCH_OFFSET = 2208988800 # Sekunden zwischen 1900 und 1970
|
||||
|
||||
|
||||
def query_ntp(server: str, timeout: float = 3.0, port: int = 123) -> float | None:
|
||||
"""Fragt einen NTP-Server und gibt das Offset (Server - Local) in
|
||||
Sekunden zurueck, oder None bei Fehler."""
|
||||
packet = b'\x1b' + 47 * b'\0' # LI=0, VN=3, Mode=3 (client)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
t0 = time.time()
|
||||
sock.sendto(packet, (server, port))
|
||||
data, _ = sock.recvfrom(1024)
|
||||
t3 = time.time()
|
||||
except (socket.gaierror, socket.timeout, OSError):
|
||||
return None
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if len(data) < 48:
|
||||
return None
|
||||
# Transmit timestamp: Offset 40, 8 bytes, fixed point 32.32
|
||||
secs, frac = struct.unpack('!II', data[40:48])
|
||||
if secs == 0:
|
||||
return None
|
||||
t2 = secs - _NTP_EPOCH_OFFSET + frac / 2**32
|
||||
# Einfacher Offset (sans roundtrip): (t2 - ((t0 + t3) / 2))
|
||||
return t2 - (t0 + t3) / 2
|
||||
|
||||
|
||||
def check_and_log(server: str, logger=None) -> float | None:
|
||||
import logging
|
||||
log = logger or logging.getLogger('ntp')
|
||||
offset = query_ntp(server)
|
||||
if offset is None:
|
||||
log.warning('NTP-Check: Server %s nicht erreichbar', server)
|
||||
return None
|
||||
if abs(offset) > 5.0:
|
||||
log.warning('NTP-Check: Systemzeit weicht um %.2fs von %s ab -> Host-Uhr synchronisieren!',
|
||||
offset, server)
|
||||
else:
|
||||
log.info('NTP-Check: Offset %.3fs gegen %s (ok)', offset, server)
|
||||
return offset
|
||||
Loading…
Reference in New Issue