diff --git a/README.md b/README.md index 3cd8add..64cb587 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,13 @@ nginx (https://schedule.our-place.ru) → 127.0.0.1:8090 фонового обновления (интервал и так перечитывает таблицу целиком); - **задать интервал обновления** в минутах (`1…1440`). +Каждое обновление (и по кнопке, и по интервалу) **всегда перечитывает таблицу**, +а в панели и в логе видно, изменился ли файл на Диске: `sha256`, время +последнего изменения содержимого и отметка «изменился / без изменений». +Если вы поправили таблицу, а после обновления написано «без изменений» — значит +публичная ссылка Яндекса отдаёт прежнюю версию файла (бывает с задержкой), +стоит повторить обновление через минуту. + Как это устроено: - вход — `POST /api/admin/login`, сессия в подписанной HMAC-cookie diff --git a/src/backend/schedule/cache.py b/src/backend/schedule/cache.py index 0c5eb79..6c8c801 100644 --- a/src/backend/schedule/cache.py +++ b/src/backend/schedule/cache.py @@ -15,6 +15,7 @@ from __future__ import annotations +import hashlib import logging import threading import time @@ -60,6 +61,10 @@ class ScheduleCache: self._modified: str | None = None self._error: str | None = None if self._link else NO_LINK_ERROR self._last_failed: float = 0.0 + # следим, менялось ли содержимое скачанного файла + self._content_hash: str | None = None + self._changed: bool = False + self._changed_at: float = 0.0 # --- публичное API --------------------------------------------------------- @@ -184,6 +189,10 @@ class ScheduleCache: "error": self._error, "groups": len(self._data) if self._data else 0, "has_data": self._data is not None, + # при последнем чтении файла: изменилось ли его содержимое + "changed": self._changed, + "changed_at": self._iso(self._changed_at), + "content_hash": (self._content_hash or "")[:12], } def _ensure_fresh(self) -> None: @@ -228,6 +237,9 @@ class ScheduleCache: log.info("Загружаю расписание по ссылке %s ...", self._link) started = time.time() xlsx = download_schedule(self._link) + digest = hashlib.sha256(xlsx).hexdigest() + first_load = self._content_hash is None + changed = not first_load and digest != self._content_hash sheets = parse_workbook(xlsx) data = parsing.parse_schedule(sheets) weeks = parsing.extract_week_ranges(sheets) @@ -241,7 +253,17 @@ class ScheduleCache: self._modified = modified self._loaded_at = now self._error = None - log.info("Расписание загружено: %d групп за %.1f с", len(data), time.time() - started) + self._content_hash = digest + self._changed = changed + if changed or first_load: + self._changed_at = now + log.info( + "Расписание загружено: %d групп за %.1f с; файл %s (sha256 %s)", + len(data), + time.time() - started, + "изменился" if changed else ("первая загрузка" if first_load else "не изменился"), + digest[:12], + ) def _fail(self, exc: ScheduleError, now: float) -> None: self._error = str(exc) diff --git a/src/backend/schedule/source.py b/src/backend/schedule/source.py index dc40451..eb92059 100644 --- a/src/backend/schedule/source.py +++ b/src/backend/schedule/source.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import time import requests @@ -12,18 +13,27 @@ log = logging.getLogger(__name__) CLOUD_API = "https://cloud-api.yandex.net/v1/disk/public/resources" +# Публичная ссылка может кэшироваться на стороне Диска: просим не кэшировать +# и добавляем в запрос метку времени, чтобы получать текущую версию файла. +_NO_CACHE = {"Cache-Control": "no-cache", "Pragma": "no-cache"} + + +def _bust() -> dict: + return {"_": int(time.time() * 1000)} + def download_schedule(public_url: str, timeout: float = 60.0) -> bytes: """Скачивает файл с диска; при любой ошибке бросает ScheduleError.""" try: resp = requests.get( f"{CLOUD_API}/download", - params={"public_key": public_url}, + params={"public_key": public_url, **_bust()}, + headers=_NO_CACHE, timeout=timeout, ) resp.raise_for_status() href = resp.json()["href"] - file_resp = requests.get(href, timeout=timeout) + file_resp = requests.get(href, headers=_NO_CACHE, timeout=timeout) file_resp.raise_for_status() return file_resp.content except (requests.RequestException, KeyError, ValueError) as exc: @@ -35,7 +45,8 @@ def fetch_table_modified(public_url: str, timeout: float = 20.0) -> str | None: try: resp = requests.get( CLOUD_API, - params={"public_key": public_url}, + params={"public_key": public_url, **_bust()}, + headers=_NO_CACHE, timeout=timeout, ) resp.raise_for_status() diff --git a/src/frontend/admin.html b/src/frontend/admin.html index 4ec6887..b38ba3d 100644 --- a/src/frontend/admin.html +++ b/src/frontend/admin.html @@ -94,7 +94,8 @@ -