diff --git a/README.md b/README.md index 302fe84..51b9205 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,18 @@ Files are backed up uncompressed by default, on the assumption a snapshotting or - `bz2` - `plain` (no compression - the default) +## Timezone support + +You can now control the timezone used for scheduled backups by setting the `TZ` environment variable. For example: + +```yaml +environment: + - TZ=Europe/Warsaw + - SCHEDULE=0 3 * * * +``` + +This ensures that the schedule is interpreted in the specified timezone. If `TZ` is not set, the system default timezone is used. + ### Example `docker-compose.yml` ```yml diff --git a/db-auto-backup.py b/db-auto-backup.py index 68200d9..930075c 100755 --- a/db-auto-backup.py +++ b/db-auto-backup.py @@ -13,6 +13,7 @@ import docker import pycron +import pytz import requests from docker.models.containers import Container from dotenv import dotenv_values @@ -158,6 +159,7 @@ def backup_redis(container: Container) -> str: BACKUP_DIR = Path(os.environ.get("BACKUP_DIR", "/var/backups")) SCHEDULE = os.environ.get("SCHEDULE", "0 0 * * *") +TZ = os.environ.get("TZ") SHOW_PROGRESS = sys.stdout.isatty() COMPRESSION = os.environ.get("COMPRESSION", "plain") INCLUDE_LOGS = bool(os.environ.get("INCLUDE_LOGS")) @@ -186,10 +188,24 @@ def get_container_names(container: Container) -> Iterable[str]: return names +def get_localized_now(dt: datetime, tz_name: str) -> datetime: + tz = pytz.timezone(tz_name) + return dt.astimezone(tz) + + @pycron.cron(SCHEDULE) def backup(now: datetime) -> None: print("Starting backup...") + # Apply timezone if TZ is set + if TZ: + try: + tz = pytz.timezone(TZ) + now = now.astimezone(tz) + except Exception as e: + print(f"Invalid timezone '{TZ}': {e}") + sys.exit(1) + docker_client = docker.from_env() containers = docker_client.containers.list() @@ -253,7 +269,7 @@ def backup(now: datetime) -> None: if __name__ == "__main__": if os.environ.get("SCHEDULE"): - print(f"Running backup with schedule '{SCHEDULE}'.") + print(f"Running backup with schedule '{SCHEDULE}' (TZ={TZ}).") pycron.start() else: backup(datetime.now()) diff --git a/dev-requirements.txt b/dev-requirements.txt index 628844c..b69fb3a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,4 +4,5 @@ black==23.12.1 ruff==0.11.8 mypy==1.15.0 types-requests +types-pytz pytest==8.3.5 diff --git a/tests/tests.py b/tests/tests.py index f59ed6e..c9feb2d 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,10 +1,12 @@ +from datetime import datetime from importlib.machinery import SourceFileLoader from importlib.util import module_from_spec, spec_from_loader from pathlib import Path from typing import Any, Callable -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest +import pytz BACKUP_DIR = Path.cwd() / "backups" @@ -130,3 +132,9 @@ def test_get_backup_provider(container_name: str, name: str) -> None: assert provider is not None assert provider.name == name + +def test_get_localized_now(): + dt = datetime(2025, 5, 27, 12, 0, 0) + localized = db_auto_backup.get_localized_now(dt, "Europe/Warsaw") + assert localized.tzinfo is not None + assert localized.tzinfo.zone == "Europe/Warsaw"