Files
xray-lists/xray_lists/fetch.py
T
2026-08-16 23:21:59 +00:00

100 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Скачивание удалённых списков с ETag-кэшем и фолбэком на локальный кэш
при недоступности источника - логика 1:1 с _fetch_url() из bash-версии.
Отдельная страховка: PySocks (используется при socks5h:// прокси) не
всегда честно соблюдает timeout, переданный в requests.get() - может
зависнуть на этапе SOCKS-хендшейка или чтения ответа. Поэтому вокруг
запроса дополнительно ставится жёсткий wall-clock таймаут через
SIGALRM, чтобы скрипт гарантированно не висел вечно на одном списке.
"""
from __future__ import annotations
import hashlib
import logging
import signal
from contextlib import contextmanager
from pathlib import Path
import requests
log = logging.getLogger("xray-lists.fetch")
class HardTimeout(Exception):
pass
@contextmanager
def _hard_timeout(seconds: int):
def _on_alarm(signum, frame):
raise HardTimeout(f"hard timeout after {seconds}s (possibly a hung proxy)")
previous = signal.signal(signal.SIGALRM, _on_alarm)
signal.alarm(seconds)
try:
yield
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, previous)
def _url_hash(url: str) -> str:
return hashlib.md5(url.encode()).hexdigest()[:8]
def build_session(proxy: str | None, proxy_user: str | None, proxy_pass: str | None) -> requests.Session:
s = requests.Session()
if proxy:
if proxy_user:
# вставляем креды в URL прокси: scheme://user:pass@host:port
scheme, rest = proxy.split("://", 1)
proxy = f"{scheme}://{proxy_user}:{proxy_pass}@{rest}"
s.proxies.update({"http": proxy, "https": proxy})
return s
def fetch_url(session: requests.Session, cache_dir: Path, entry_id: str, url: str,
timeout: int) -> str | None:
"""
Возвращает текстовое содержимое URL, используя ETag-кэш.
None означает "данных нет вообще" (сеть недоступна и кэша тоже нет).
"""
cache_dir.mkdir(parents=True, exist_ok=True)
h = _url_hash(url)
raw_cache = cache_dir / f"{entry_id}_{h}.raw"
etag_file = cache_dir / f"{entry_id}_{h}.etag"
headers = {}
etag = None
if etag_file.is_file():
etag = etag_file.read_text().strip()
if etag:
headers["If-None-Match"] = f'"{etag}"'
try:
# +5s запас сверх requests-таймаута: если requests/PySocks сами
# отработают штатно, alarm просто снимется в finally и не выстрелит.
with _hard_timeout(timeout + 5):
resp = session.get(url, headers=headers, timeout=timeout)
except (requests.RequestException, HardTimeout) as exc:
log.warning("%s: download failed (%s), using local cache if any", entry_id, exc)
return raw_cache.read_text() if raw_cache.is_file() else None
if resp.status_code == 304:
log.info("%s: 304 not modified (%s)", entry_id, url)
return raw_cache.read_text() if raw_cache.is_file() else None
if resp.status_code == 200:
log.info("%s: 200 ok (%s)", entry_id, url)
new_etag = resp.headers.get("ETag", "").strip().strip('"').lstrip("W/")
if new_etag:
etag_file.write_text(new_etag)
raw_cache.write_text(resp.text)
return resp.text
log.warning("%s: unexpected status %s for %s, using local cache if any",
entry_id, resp.status_code, url)
return raw_cache.read_text() if raw_cache.is_file() else None