缓存
This commit is contained in:
@@ -23,11 +23,13 @@ class Settings(BaseSettings):
|
||||
public_base_url: str | None = None
|
||||
request_timeout_seconds: float = 20.0
|
||||
cache_ttl_seconds: int = 900
|
||||
bundle_cache_ttl_seconds: int = 600
|
||||
max_proxy_name_length: int = 80
|
||||
default_user_agent: str = "sub-provider/0.2"
|
||||
|
||||
sources_file: Path = CONFIG_DIR / "sources.yaml"
|
||||
rules_dir: Path = CONFIG_DIR / "rules"
|
||||
bundle_cache_dir: Path = ROOT_DIR / "output" / "bundle-cache"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=ROOT_DIR / ".env",
|
||||
|
||||
47
app/main.py
47
app/main.py
@@ -4,7 +4,8 @@ from fastapi import FastAPI, HTTPException, Query, Request
|
||||
from fastapi.responses import Response
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models import RuleConfig, SourceConfig
|
||||
from app.models import RuleConfig, SourceConfig, SourceSnapshot
|
||||
from app.services.bundle_cache import build_bundle_cache_key, load_bundle_cache, save_bundle_cache
|
||||
from app.services.loader import load_app_config
|
||||
from app.services.profiles import build_bundle_profile, build_thin_profile, dump_yaml
|
||||
from app.services.rules import load_rule_text
|
||||
@@ -71,6 +72,15 @@ async def _build_quota_headers(source_items: list[tuple[str, SourceConfig]]) ->
|
||||
return headers
|
||||
|
||||
|
||||
def _quota_headers_from_snapshots(snapshots: list[SourceSnapshot]) -> dict[str, str]:
|
||||
if not snapshots:
|
||||
return {}
|
||||
quota = snapshots[0].quota
|
||||
if quota and not quota.is_empty():
|
||||
return {"Subscription-Userinfo": quota.to_header_value()}
|
||||
return {}
|
||||
|
||||
|
||||
def _yaml_response(content: str, request: Request, headers: dict[str, str] | None = None, filename: str | None = None) -> Response:
|
||||
final_headers = {
|
||||
"Content-Type": "text/yaml; charset=utf-8",
|
||||
@@ -145,12 +155,32 @@ async def client_profile(client_type: str, request: Request, sources: str | None
|
||||
|
||||
|
||||
@app.api_route(PUBLIC_PREFIX + "/bundle/{client_type}.yaml", methods=["GET", "HEAD"])
|
||||
async def bundle_profile(client_type: str, request: Request, sources: str | None = Query(default=None)) -> Response:
|
||||
async def bundle_profile(
|
||||
client_type: str,
|
||||
request: Request,
|
||||
sources: str | None = Query(default=None),
|
||||
force_refresh: bool = Query(default=False),
|
||||
) -> Response:
|
||||
client = app_config.clients.get(client_type)
|
||||
if client is None:
|
||||
raise HTTPException(status_code=404, detail="client config not found")
|
||||
|
||||
source_items = _resolve_sources(sources)
|
||||
cache_key = build_bundle_cache_key(client_type=client_type, source_names=[name for name, _ in source_items])
|
||||
if not force_refresh:
|
||||
cached = load_bundle_cache(
|
||||
cache_dir=settings.bundle_cache_dir,
|
||||
cache_key=cache_key,
|
||||
ttl_seconds=settings.bundle_cache_ttl_seconds,
|
||||
)
|
||||
if cached is not None:
|
||||
headers = {
|
||||
"profile-update-interval": str(client.provider_interval),
|
||||
"X-Sub-Provider-Bundle-Cache": "HIT",
|
||||
}
|
||||
headers.update(cached.headers)
|
||||
return _yaml_response(content=cached.content, request=request, headers=headers, filename=f"bundle-{client_type}.yaml")
|
||||
|
||||
try:
|
||||
snapshots = await build_source_snapshots(source_items)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -164,6 +194,15 @@ async def bundle_profile(client_type: str, request: Request, sources: str | None
|
||||
snapshots=snapshots,
|
||||
)
|
||||
)
|
||||
headers = {"profile-update-interval": str(client.provider_interval)}
|
||||
headers.update(await _build_quota_headers(source_items))
|
||||
headers = {
|
||||
"profile-update-interval": str(client.provider_interval),
|
||||
"X-Sub-Provider-Bundle-Cache": "BYPASS" if force_refresh else "MISS",
|
||||
}
|
||||
headers.update(_quota_headers_from_snapshots(snapshots))
|
||||
save_bundle_cache(
|
||||
cache_dir=settings.bundle_cache_dir,
|
||||
cache_key=cache_key,
|
||||
content=content,
|
||||
headers={key: value for key, value in headers.items() if key != "X-Sub-Provider-Bundle-Cache"},
|
||||
)
|
||||
return _yaml_response(content, request, headers=headers, filename=f"bundle-{client_type}.yaml")
|
||||
|
||||
54
app/services/bundle_cache.py
Normal file
54
app/services/bundle_cache.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BundleCacheEntry(BaseModel):
|
||||
content: str
|
||||
headers: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
def build_bundle_cache_key(*, client_type: str, source_names: list[str]) -> str:
|
||||
raw = f"{client_type}|{','.join(source_names)}"
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def load_bundle_cache(*, cache_dir: Path, cache_key: str, ttl_seconds: int) -> BundleCacheEntry | None:
|
||||
yaml_path = cache_dir / f"{cache_key}.yaml"
|
||||
meta_path = cache_dir / f"{cache_key}.json"
|
||||
if not yaml_path.is_file() or not meta_path.is_file():
|
||||
return None
|
||||
|
||||
expires_at = meta_path.stat().st_mtime + ttl_seconds
|
||||
if expires_at < time.time():
|
||||
return None
|
||||
|
||||
try:
|
||||
metadata = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
content = yaml_path.read_text(encoding="utf-8")
|
||||
headers = metadata.get("headers")
|
||||
if not isinstance(headers, dict):
|
||||
headers = {}
|
||||
return BundleCacheEntry(content=content, headers={str(k): str(v) for k, v in headers.items()})
|
||||
|
||||
|
||||
def save_bundle_cache(
|
||||
*,
|
||||
cache_dir: Path,
|
||||
cache_key: str,
|
||||
content: str,
|
||||
headers: dict[str, str],
|
||||
) -> Path:
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
yaml_path = cache_dir / f"{cache_key}.yaml"
|
||||
meta_path = cache_dir / f"{cache_key}.json"
|
||||
yaml_path.write_text(content, encoding="utf-8")
|
||||
meta_path.write_text(json.dumps({"headers": headers}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return yaml_path
|
||||
Reference in New Issue
Block a user