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>
89 lines
3.6 KiB
Python
89 lines
3.6 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
from l4d2web.db import init_db, session_scope
|
|
from l4d2web.models import GlobalOverlayItem, GlobalOverlayItemFile, GlobalOverlaySource, Overlay
|
|
from l4d2web.services.overlay_builders import BUILDERS
|
|
|
|
|
|
def seed_source(tmp_path: Path, monkeypatch) -> int:
|
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'builder.db'}")
|
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
|
init_db()
|
|
cache_vpk = tmp_path / "global_overlay_cache" / "l4d2center-maps" / "vpks" / "carriedoff.vpk"
|
|
cache_vpk.parent.mkdir(parents=True, exist_ok=True)
|
|
cache_vpk.write_bytes(b"vpk")
|
|
with session_scope() as db:
|
|
overlay = Overlay(name="l4d2center-maps", path="7", type="l4d2center_maps", user_id=None)
|
|
db.add(overlay)
|
|
db.flush()
|
|
source = GlobalOverlaySource(
|
|
overlay_id=overlay.id,
|
|
source_key="l4d2center-maps",
|
|
source_type="l4d2center_csv",
|
|
source_url="https://l4d2center.com/maps/servers/index.csv",
|
|
)
|
|
db.add(source)
|
|
db.flush()
|
|
item = GlobalOverlayItem(
|
|
source_id=source.id,
|
|
item_key="carriedoff.vpk",
|
|
display_name="carriedoff.vpk",
|
|
download_url="https://example.invalid/carriedoff.7z",
|
|
expected_vpk_name="carriedoff.vpk",
|
|
)
|
|
db.add(item)
|
|
db.flush()
|
|
db.add(
|
|
GlobalOverlayItemFile(
|
|
item_id=item.id,
|
|
vpk_name="carriedoff.vpk",
|
|
cache_path="l4d2center-maps/vpks/carriedoff.vpk",
|
|
size=3,
|
|
md5="",
|
|
)
|
|
)
|
|
db.flush()
|
|
return overlay.id
|
|
|
|
|
|
def test_registry_contains_global_map_builders():
|
|
assert "l4d2center_maps" in BUILDERS
|
|
assert "cedapug_maps" in BUILDERS
|
|
|
|
|
|
def test_global_builder_creates_absolute_symlink(tmp_path, monkeypatch):
|
|
overlay_id = seed_source(tmp_path, monkeypatch)
|
|
out: list[str] = []
|
|
err: list[str] = []
|
|
with session_scope() as db:
|
|
overlay = db.query(Overlay).filter_by(id=overlay_id).one()
|
|
BUILDERS["l4d2center_maps"].build(overlay, on_stdout=out.append, on_stderr=err.append, should_cancel=lambda: False)
|
|
|
|
link = tmp_path / "overlays" / "7" / "left4dead2" / "addons" / "carriedoff.vpk"
|
|
assert link.is_symlink()
|
|
assert os.path.isabs(os.readlink(link))
|
|
assert link.resolve() == (tmp_path / "global_overlay_cache" / "l4d2center-maps" / "vpks" / "carriedoff.vpk").resolve()
|
|
assert any("global overlay" in line for line in out)
|
|
|
|
|
|
def test_global_builder_removes_obsolete_managed_symlink_but_keeps_foreign(tmp_path, monkeypatch):
|
|
overlay_id = seed_source(tmp_path, monkeypatch)
|
|
addons = tmp_path / "overlays" / "7" / "left4dead2" / "addons"
|
|
addons.mkdir(parents=True, exist_ok=True)
|
|
foreign_target = tmp_path / "foreign.vpk"
|
|
foreign_target.write_bytes(b"foreign")
|
|
os.symlink(str(foreign_target), addons / "foreign.vpk")
|
|
|
|
with session_scope() as db:
|
|
overlay = db.query(Overlay).filter_by(id=overlay_id).one()
|
|
BUILDERS["l4d2center_maps"].build(overlay, on_stdout=lambda line: None, on_stderr=lambda line: None, should_cancel=lambda: False)
|
|
source = db.query(GlobalOverlaySource).filter_by(source_key="l4d2center-maps").one()
|
|
db.query(GlobalOverlayItem).filter_by(source_id=source.id).delete()
|
|
|
|
with session_scope() as db:
|
|
overlay = db.query(Overlay).filter_by(id=overlay_id).one()
|
|
BUILDERS["l4d2center_maps"].build(overlay, on_stdout=lambda line: None, on_stderr=lambda line: None, should_cancel=lambda: False)
|
|
|
|
assert not (addons / "carriedoff.vpk").exists()
|
|
assert (addons / "foreign.vpk").is_symlink()
|