custom_components/bahmcloud_store/views.py aktualisiert
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -9,6 +8,8 @@ from typing import Any, TYPE_CHECKING
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
|
from .providers import fetch_readme_markdown_from_repo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .core import BCSCore # typing only
|
from .core import BCSCore # typing only
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ def _render_markdown_server_side(md: str) -> str | None:
|
|||||||
|
|
||||||
html: str | None = None
|
html: str | None = None
|
||||||
|
|
||||||
# 1) python-markdown
|
|
||||||
try:
|
try:
|
||||||
import markdown as mdlib # type: ignore
|
import markdown as mdlib # type: ignore
|
||||||
|
|
||||||
@@ -39,7 +39,6 @@ def _render_markdown_server_side(md: str) -> str | None:
|
|||||||
if not html:
|
if not html:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 2) Sanitize via bleach
|
|
||||||
try:
|
try:
|
||||||
import bleach # type: ignore
|
import bleach # type: ignore
|
||||||
|
|
||||||
@@ -105,94 +104,6 @@ def _render_markdown_server_side(md: str) -> str | None:
|
|||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
_TEXT_KEYS = ("readme", "markdown", "text", "content", "data", "body")
|
|
||||||
|
|
||||||
|
|
||||||
def _maybe_decode_base64(content: str, encoding: Any) -> str | None:
|
|
||||||
if not isinstance(content, str):
|
|
||||||
return None
|
|
||||||
enc = ""
|
|
||||||
if isinstance(encoding, str):
|
|
||||||
enc = encoding.strip().lower()
|
|
||||||
if "base64" not in enc:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
raw = base64.b64decode(content.encode("utf-8"), validate=False)
|
|
||||||
return raw.decode("utf-8", errors="replace")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_text_recursive(obj: Any, depth: int = 0) -> str | None:
|
|
||||||
"""
|
|
||||||
Robust extraction for README markdown.
|
|
||||||
|
|
||||||
Handles:
|
|
||||||
- str / bytes
|
|
||||||
- dict with:
|
|
||||||
- {content: "...", encoding: "base64"} (possibly nested)
|
|
||||||
- {readme: "..."} etc.
|
|
||||||
- list of dicts (pick first matching)
|
|
||||||
"""
|
|
||||||
if obj is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(obj, bytes):
|
|
||||||
try:
|
|
||||||
return obj.decode("utf-8", errors="replace")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(obj, str):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
if depth > 8:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
# 1) If it looks like "file content"
|
|
||||||
content = obj.get("content")
|
|
||||||
encoding = obj.get("encoding")
|
|
||||||
|
|
||||||
# Base64 decode if possible
|
|
||||||
decoded = _maybe_decode_base64(content, encoding)
|
|
||||||
if decoded:
|
|
||||||
return decoded
|
|
||||||
|
|
||||||
# content may already be plain text
|
|
||||||
if isinstance(content, str) and (not isinstance(encoding, str) or not encoding.strip()):
|
|
||||||
# Heuristic: treat as markdown if it has typical markdown chars, otherwise still return
|
|
||||||
return content
|
|
||||||
|
|
||||||
# 2) direct text keys (readme/markdown/text/body/data)
|
|
||||||
for k in _TEXT_KEYS:
|
|
||||||
v = obj.get(k)
|
|
||||||
if isinstance(v, str):
|
|
||||||
return v
|
|
||||||
if isinstance(v, bytes):
|
|
||||||
try:
|
|
||||||
return v.decode("utf-8", errors="replace")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 3) Sometimes nested under "file" / "result" / "payload" etc.
|
|
||||||
for v in obj.values():
|
|
||||||
out = _extract_text_recursive(v, depth + 1)
|
|
||||||
if out:
|
|
||||||
return out
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(obj, list):
|
|
||||||
for item in obj:
|
|
||||||
out = _extract_text_recursive(item, depth + 1)
|
|
||||||
if out:
|
|
||||||
return out
|
|
||||||
return None
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class StaticAssetsView(HomeAssistantView):
|
class StaticAssetsView(HomeAssistantView):
|
||||||
url = "/api/bahmcloud_store_static/{path:.*}"
|
url = "/api/bahmcloud_store_static/{path:.*}"
|
||||||
name = "api:bahmcloud_store_static"
|
name = "api:bahmcloud_store_static"
|
||||||
@@ -299,18 +210,33 @@ class BCSReadmeView(HomeAssistantView):
|
|||||||
if not repo_id:
|
if not repo_id:
|
||||||
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
|
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
|
||||||
|
|
||||||
maybe_md = await self.core.fetch_readme_markdown(repo_id)
|
repos = self.core.list_repos_public()
|
||||||
|
repo = next((r for r in repos if str(r.get("id", "")) == str(repo_id)), None)
|
||||||
|
if not repo:
|
||||||
|
return web.json_response({"ok": False, "message": "Repository not found."}, status=404)
|
||||||
|
|
||||||
|
repo_url = repo.get("url")
|
||||||
|
provider = repo.get("provider")
|
||||||
|
default_branch = repo.get("default_branch")
|
||||||
|
|
||||||
|
if not isinstance(repo_url, str) or not repo_url.strip():
|
||||||
|
return web.json_response({"ok": False, "message": "Repository URL missing."}, status=404)
|
||||||
|
|
||||||
|
md = await fetch_readme_markdown_from_repo(
|
||||||
|
self.core.hass,
|
||||||
|
repo_url=repo_url,
|
||||||
|
provider=provider if isinstance(provider, str) else None,
|
||||||
|
default_branch=default_branch if isinstance(default_branch, str) else None,
|
||||||
|
)
|
||||||
|
|
||||||
md = _extract_text_recursive(maybe_md)
|
|
||||||
if not md or not md.strip():
|
if not md or not md.strip():
|
||||||
t = type(maybe_md).__name__
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"ok": False, "message": f"README not found or unsupported format (got {t})."},
|
{
|
||||||
|
"ok": False,
|
||||||
|
"message": "README not found (raw endpoint returned 404).",
|
||||||
|
},
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure strict JSON string output (avoid accidental objects)
|
html = _render_markdown_server_side(md)
|
||||||
md_str = str(md)
|
return web.json_response({"ok": True, "readme": md, "html": html})
|
||||||
|
|
||||||
html = _render_markdown_server_side(md_str)
|
|
||||||
return web.json_response({"ok": True, "readme": md_str, "html": html})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user