Adds two managed system overlays (l4d2center-maps, cedapug-maps) that fetch curated map archives from upstream sources and reconcile addons symlinks for non-Steam maps. A daily systemd timer enqueues a coalesced refresh_global_overlays worker job; downloads, extraction, and rebuilds run in the existing job worker and surface in the job log UI. Schema: GlobalOverlaySource / GlobalOverlayItem / GlobalOverlayItemFile plus nullable Job.user_id so system jobs render as "system" in the UI. The new builder reconciles symlinks against the per-source vpk cache and leaves foreign symlinks untouched. Initialize-time guard refuses to mount a partial overlay if any expected vpk is missing from cache. Refresh service uses shutil.move to handle EXDEV when /tmp and the cache live on different filesystems. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
3.3 KiB
Python
112 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import os
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from l4d2host.paths import get_left4me_root
|
|
|
|
from l4d2web.models import GlobalOverlaySource, Job, Overlay
|
|
from l4d2web.services.overlay_creation import generate_overlay_path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ManagedGlobalOverlay:
|
|
name: str
|
|
overlay_type: str
|
|
source_type: str
|
|
source_url: str
|
|
|
|
|
|
GLOBAL_OVERLAYS = (
|
|
ManagedGlobalOverlay(
|
|
name="l4d2center-maps",
|
|
overlay_type="l4d2center_maps",
|
|
source_type="l4d2center_csv",
|
|
source_url="https://l4d2center.com/maps/servers/index.csv",
|
|
),
|
|
ManagedGlobalOverlay(
|
|
name="cedapug-maps",
|
|
overlay_type="cedapug_maps",
|
|
source_type="cedapug_custom_page",
|
|
source_url="https://cedapug.com/custom",
|
|
),
|
|
)
|
|
|
|
MANAGED_GLOBAL_OVERLAY_TYPES = {overlay.overlay_type for overlay in GLOBAL_OVERLAYS}
|
|
USER_CREATABLE_TYPES = {"workshop"}
|
|
ADMIN_CREATABLE_TYPES = {"external", "workshop"}
|
|
|
|
|
|
def is_creatable_overlay_type(overlay_type: str, *, admin: bool) -> bool:
|
|
allowed = ADMIN_CREATABLE_TYPES if admin else USER_CREATABLE_TYPES
|
|
return overlay_type in allowed
|
|
|
|
|
|
def ensure_global_overlays(session: Session) -> set[str]:
|
|
created_sources: set[str] = set()
|
|
for managed in GLOBAL_OVERLAYS:
|
|
overlay = session.scalar(
|
|
select(Overlay).where(Overlay.name == managed.name, Overlay.user_id.is_(None))
|
|
)
|
|
overlay_created = overlay is None
|
|
if overlay is None:
|
|
overlay = Overlay(name=managed.name, path="", type=managed.overlay_type, user_id=None)
|
|
session.add(overlay)
|
|
session.flush()
|
|
overlay.path = generate_overlay_path(overlay.id)
|
|
else:
|
|
overlay.type = managed.overlay_type
|
|
overlay.user_id = None
|
|
if not overlay.path:
|
|
overlay.path = generate_overlay_path(overlay.id)
|
|
|
|
target = get_left4me_root() / "overlays" / overlay.path
|
|
os.makedirs(target, exist_ok=not overlay_created)
|
|
|
|
source = session.scalar(
|
|
select(GlobalOverlaySource).where(GlobalOverlaySource.source_key == managed.name)
|
|
)
|
|
if source is None:
|
|
source = GlobalOverlaySource(
|
|
overlay_id=overlay.id,
|
|
source_key=managed.name,
|
|
source_type=managed.source_type,
|
|
source_url=managed.source_url,
|
|
)
|
|
session.add(source)
|
|
created_sources.add(managed.name)
|
|
else:
|
|
source.overlay_id = overlay.id
|
|
source.source_type = managed.source_type
|
|
source.source_url = managed.source_url
|
|
|
|
session.flush()
|
|
|
|
return created_sources
|
|
|
|
|
|
def enqueue_refresh_global_overlays(session: Session, *, user_id: int | None) -> Job:
|
|
existing = session.scalar(
|
|
select(Job)
|
|
.where(
|
|
Job.operation == "refresh_global_overlays",
|
|
Job.state.in_({"queued", "running", "cancelling"}),
|
|
)
|
|
.order_by(Job.created_at, Job.id)
|
|
)
|
|
if existing is not None:
|
|
return existing
|
|
|
|
job = Job(
|
|
user_id=user_id,
|
|
server_id=None,
|
|
overlay_id=None,
|
|
operation="refresh_global_overlays",
|
|
state="queued",
|
|
)
|
|
session.add(job)
|
|
session.flush()
|
|
return job
|