diff --git a/docs/size-exemptions.md b/docs/size-exemptions.md index 2f3f7db..ae37226 100644 --- a/docs/size-exemptions.md +++ b/docs/size-exemptions.md @@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable. ## Planned, NOT exempt (owned by split plans — listed so nothing falls between states) core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py` -(7,275 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` -extractions and twelve `routers/` modules) · +(7,216 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` +extractions and thirteen `routers/` modules) · `lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines and is a monolith in its own right, to be split per-table once the router train lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js` diff --git a/lib/routers/version.py b/lib/routers/version.py new file mode 100644 index 0000000..45403de --- /dev/null +++ b/lib/routers/version.py @@ -0,0 +1,81 @@ +"""App version + source/license URLs (/api/version). + +Extracted verbatim from ``server.py`` (R3) except the decorator (``@app`` -> +``@router``) and the VERSION-file lookup: ``Path(__file__).parent`` (app root +when this lived at the top level) -> ``Path(__file__).resolve().parents[2]`` +(routers -> lib -> app root). VERSION ships at the app root in every packaging +path (Dockerfile COPY, desktop bundle). +""" + +import os +from pathlib import Path + +from fastapi import APIRouter + +router = APIRouter() + + +def _safe_http_url(raw): + """Return `raw` stripped + trailing-slash-stripped if it parses as an + http(s) URL with a non-empty host; else None. + + Used to validate operator-supplied `APP_SOURCE_URL` / `APP_LICENSE_URL` + env vars before they reach `` in the UI. A bare prefix check + like `startswith(("http://","https://"))` accepts malformed inputs + such as `"https://"` (no host) or `"https:///foo"` (empty host) that + still produce broken hrefs — and, when used as a base for the default + `license_url`, garbage like `"https:///blob/main/LICENSE"`. + """ + from urllib.parse import urlsplit + if not raw: + return None + s = raw.strip().rstrip("/") + if not s: + return None + try: + parsed = urlsplit(s) + except ValueError: + return None + if parsed.scheme.lower() not in ("http", "https"): + return None + # `netloc` includes any `user:pass@` and `:port` — strings like + # "http://:80/path" have non-empty netloc (":80") but no real + # hostname. Validate `hostname` so only URLs with an actual host + # are accepted. + if not parsed.hostname: + return None + return s + + +@router.get("/api/version") +def get_version(): + env_version = os.environ.get("APP_VERSION", "").strip() + if env_version: + version = env_version + else: + version_file = Path(__file__).resolve().parents[2] / "VERSION" # R3: app root from lib/routers/ + version = "unknown" + if version_file.exists(): + try: + version = version_file.read_text().strip() + except (OSError, UnicodeDecodeError): + pass + default_source_url = "https://github.com/got-feedback/feedBack" + # APP_SOURCE_URL / APP_LICENSE_URL flow straight into in the UI, + # so validate with urllib.parse rather than a bare prefix check — a prefix + # check accepts malformed values like "https://" (no host) which produce + # broken hrefs (and a constructed license_url like "https:///blob/main/LICENSE"). + # _safe_http_url requires scheme in {http,https} AND a non-empty hostname + # (not just netloc — that would still accept port-only authorities like + # "http://:80/path"); fall back to the safe default otherwise. + source_url = _safe_http_url(os.environ.get("APP_SOURCE_URL")) or default_source_url + # APP_LICENSE_URL: explicit override for the LICENSE link. The default + # constructed value (source_url + "/blob/main/LICENSE") is GitHub- + # specific and assumes the repo's default branch is `main`; non-GitHub + # hosts (GitLab, Gitea, self-hosted) need an explicit value. + license_url = _safe_http_url(os.environ.get("APP_LICENSE_URL")) or (source_url + "/blob/main/LICENSE") + return { + "version": version, + "source_url": source_url, + "license_url": license_url, + } diff --git a/server.py b/server.py index a65e72b..01bd11e 100644 --- a/server.py +++ b/server.py @@ -55,7 +55,7 @@ from dlc_paths import _get_dlc_dir, _resolve_dlc_path # Lives in lib/ because that is the one core dir every packaging path copies. import appstate # Extracted route modules. They import `appstate`, never `server` — one-way graph. -from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras, shop, progression, profile, stats +from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras, shop, progression, profile, stats, version import sloppak as sloppak_mod import loosefolder as loosefolder_mod # Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/ @@ -3193,70 +3193,11 @@ def _periodic_rescan(): time.sleep(300) -def _safe_http_url(raw): - """Return `raw` stripped + trailing-slash-stripped if it parses as an - http(s) URL with a non-empty host; else None. - - Used to validate operator-supplied `APP_SOURCE_URL` / `APP_LICENSE_URL` - env vars before they reach `` in the UI. A bare prefix check - like `startswith(("http://","https://"))` accepts malformed inputs - such as `"https://"` (no host) or `"https:///foo"` (empty host) that - still produce broken hrefs — and, when used as a base for the default - `license_url`, garbage like `"https:///blob/main/LICENSE"`. - """ - from urllib.parse import urlsplit - if not raw: - return None - s = raw.strip().rstrip("/") - if not s: - return None - try: - parsed = urlsplit(s) - except ValueError: - return None - if parsed.scheme.lower() not in ("http", "https"): - return None - # `netloc` includes any `user:pass@` and `:port` — strings like - # "http://:80/path" have non-empty netloc (":80") but no real - # hostname. Validate `hostname` so only URLs with an actual host - # are accepted. - if not parsed.hostname: - return None - return s -@app.get("/api/version") -def get_version(): - env_version = os.environ.get("APP_VERSION", "").strip() - if env_version: - version = env_version - else: - version_file = Path(__file__).parent / "VERSION" - version = "unknown" - if version_file.exists(): - try: - version = version_file.read_text().strip() - except (OSError, UnicodeDecodeError): - pass - default_source_url = "https://github.com/got-feedback/feedBack" - # APP_SOURCE_URL / APP_LICENSE_URL flow straight into in the UI, - # so validate with urllib.parse rather than a bare prefix check — a prefix - # check accepts malformed values like "https://" (no host) which produce - # broken hrefs (and a constructed license_url like "https:///blob/main/LICENSE"). - # _safe_http_url requires scheme in {http,https} AND a non-empty hostname - # (not just netloc — that would still accept port-only authorities like - # "http://:80/path"); fall back to the safe default otherwise. - source_url = _safe_http_url(os.environ.get("APP_SOURCE_URL")) or default_source_url - # APP_LICENSE_URL: explicit override for the LICENSE link. The default - # constructed value (source_url + "/blob/main/LICENSE") is GitHub- - # specific and assumes the repo's default branch is `main`; non-GitHub - # hosts (GitLab, Gitea, self-hosted) need an explicit value. - license_url = _safe_http_url(os.environ.get("APP_LICENSE_URL")) or (source_url + "/blob/main/LICENSE") - return { - "version": version, - "source_url": source_url, - "license_url": license_url, - } +# ── App version / source URLs ──────────────────────────────────────────────── +# Mounted here (registration order). Implementation in lib/routers/version.py. +app.include_router(version.router) @app.get("/api/scan-status")