mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	fix: Convert Daily Schedule Time to UTC (#3914)
This commit is contained in:
		| @@ -5,19 +5,19 @@ | ||||
| ### General | ||||
|  | ||||
| | Variables                     |        Default        | Description                                                                                               | | ||||
| | ----------------------------- | :-------------------: | ----------------------------------------------------------------------------------- | | ||||
| | ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- | | ||||
| | PUID                          |          911          | UserID permissions between host OS and container                                                          | | ||||
| | PGID                          |          911          | GroupID permissions between host OS and container                                                         | | ||||
| | DEFAULT_GROUP                 |         Home          | The default group for users                                                                               | | ||||
| | BASE_URL                      | http://localhost:8080 | Used for Notifications                                                                                    | | ||||
| | TOKEN_TIME                    |          48           | The time in hours that a login/auth token is valid                                                        | | ||||
| | API_PORT                      |         9000          | The port exposed by backend API. **Do not change this if you're running in Docker**                       | | ||||
| | API_DOCS                      |         True          | Turns on/off access to the API documentation locally.                               | | ||||
| | API_DOCS                      |         True          | Turns on/off access to the API documentation locally                                                      | | ||||
| | TZ                            |          UTC          | Must be set to get correct date/time on the server                                                        | | ||||
| | ALLOW_SIGNUP<super>\*</super> |         false         | Allow user sign-up without token                                                                          | | ||||
| | LOG_CONFIG_OVERRIDE           |                       | Override the config for logging with a custom path                                                        | | ||||
| | LOG_LEVEL                     |         info          | Logging level (e.g. critical, error, warning, info, debug, trace)                                         | | ||||
| | DAILY_SCHEDULE_TIME           |         23:45         | The time of day to run the daily tasks.                                             | | ||||
| | DAILY_SCHEDULE_TIME           |         23:45         | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC        | | ||||
|  | ||||
| <super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application. | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| import logging | ||||
| import secrets | ||||
| from datetime import datetime, timezone | ||||
| from pathlib import Path | ||||
| from typing import NamedTuple | ||||
|  | ||||
| from dateutil.tz import tzlocal | ||||
| from pydantic import field_validator | ||||
| from pydantic_settings import BaseSettings, SettingsConfigDict | ||||
|  | ||||
| @@ -9,6 +13,11 @@ from mealie.core.settings.themes import Theme | ||||
| from .db_providers import AbstractDBProvider, db_provider_factory | ||||
|  | ||||
|  | ||||
| class ScheduleTime(NamedTuple): | ||||
|     hour: int | ||||
|     minute: int | ||||
|  | ||||
|  | ||||
| def determine_secrets(data_dir: Path, production: bool) -> str: | ||||
|     if not production: | ||||
|         return "shh-secret-test-key" | ||||
| @@ -58,6 +67,44 @@ class AppSettings(BaseSettings): | ||||
|     ALLOW_SIGNUP: bool = False | ||||
|  | ||||
|     DAILY_SCHEDULE_TIME: str = "23:45" | ||||
|     """Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent""" | ||||
|  | ||||
|     _logger: logging.Logger | None = None | ||||
|  | ||||
|     @property | ||||
|     def logger(self) -> logging.Logger: | ||||
|         if self._logger is None: | ||||
|             # lazy load the logger, since get_logger depends on the settings being loaded | ||||
|             from mealie.core.root_logger import get_logger | ||||
|  | ||||
|             self._logger = get_logger() | ||||
|  | ||||
|         return self._logger | ||||
|  | ||||
|     @property | ||||
|     def DAILY_SCHEDULE_TIME_UTC(self) -> ScheduleTime: | ||||
|         """The DAILY_SCHEDULE_TIME in UTC, parsed into hours and minutes""" | ||||
|  | ||||
|         # parse DAILY_SCHEDULE_TIME into hours and minutes | ||||
|         try: | ||||
|             hour_str, minute_str = self.DAILY_SCHEDULE_TIME.split(":") | ||||
|             local_hour = int(hour_str) | ||||
|             local_minute = int(minute_str) | ||||
|         except ValueError: | ||||
|             local_hour = 23 | ||||
|             local_minute = 45 | ||||
|             self.logger.exception( | ||||
|                 f"Unable to parse {self.DAILY_SCHEDULE_TIME=} as HH:MM; defaulting to {local_hour}:{local_minute}" | ||||
|             ) | ||||
|  | ||||
|         # DAILY_SCHEDULE_TIME is in local time, so we convert it to UTC | ||||
|         local_tz = tzlocal() | ||||
|         now = datetime.now(local_tz) | ||||
|         local_time = now.replace(hour=local_hour, minute=local_minute) | ||||
|         utc_time = local_time.astimezone(timezone.utc) | ||||
|  | ||||
|         self.logger.debug(f"Local time: {local_hour}:{local_minute} | UTC time: {utc_time.hour}:{utc_time.minute}") | ||||
|         return ScheduleTime(utc_time.hour, utc_time.minute) | ||||
|  | ||||
|     # =============================================== | ||||
|     # Security Configuration | ||||
|   | ||||
| @@ -29,20 +29,12 @@ class SchedulerService: | ||||
|  | ||||
| async def schedule_daily(): | ||||
|     now = datetime.now(timezone.utc) | ||||
|     daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME | ||||
|     logger.debug( | ||||
|         "Current time is %s and DAILY_SCHEDULE_TIME is %s", | ||||
|         str(now), | ||||
|         daily_schedule_time, | ||||
|     ) | ||||
|     try: | ||||
|         hour_target, minute_target = _parse_daily_schedule_time(daily_schedule_time) | ||||
|     except Exception: | ||||
|         logger.exception(f"Unable to parse {daily_schedule_time=}") | ||||
|         hour_target = 23 | ||||
|         minute_target = 45 | ||||
|     daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME_UTC | ||||
|     logger.debug(f"Current time is {now} and DAILY_SCHEDULE_TIME (in UTC) is {daily_schedule_time}") | ||||
|  | ||||
|     next_schedule = now.replace(hour=hour_target, minute=minute_target, second=0, microsecond=0) | ||||
|     next_schedule = now.replace( | ||||
|         hour=daily_schedule_time.hour, minute=daily_schedule_time.minute, second=0, microsecond=0 | ||||
|     ) | ||||
|     delta = next_schedule - now | ||||
|     if delta < timedelta(0): | ||||
|         next_schedule = next_schedule + timedelta(days=1) | ||||
| @@ -61,12 +53,6 @@ async def schedule_daily(): | ||||
|     await run_daily() | ||||
|  | ||||
|  | ||||
| def _parse_daily_schedule_time(time): | ||||
|     hour_target = int(time.split(":")[0]) | ||||
|     minute_target = int(time.split(":")[1]) | ||||
|     return hour_target, minute_target | ||||
|  | ||||
|  | ||||
| def _scheduled_task_wrapper(callable): | ||||
|     try: | ||||
|         callable() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user