custom_components/bahmcloud_store/store.py aktualisiert
This commit is contained in:
@@ -35,7 +35,7 @@ class StoreConfig:
|
||||
class Package:
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
type: str # "integration" | "store"
|
||||
domain: str
|
||||
repo: str
|
||||
owner: str
|
||||
@@ -76,12 +76,19 @@ class BahmcloudStore:
|
||||
u = urlparse(repo_url.rstrip("/"))
|
||||
return f"{u.scheme}://{u.netloc}"
|
||||
|
||||
@staticmethod
|
||||
def _raw_manifest_url(repo: str, branch: str, source_path: str) -> str:
|
||||
# Example:
|
||||
# https://git.bahmcloud.de/bahmcloud/easy_proxmox/raw/branch/main/custom_components/easy_proxmox/manifest.json
|
||||
return f"{repo.rstrip('/')}/raw/branch/{branch}/{source_path.rstrip('/')}/manifest.json"
|
||||
|
||||
async def _fetch_latest_version(self, pkg: Package) -> tuple[str | None, str | None]:
|
||||
"""
|
||||
Returns (latest_version, release_url)
|
||||
Strategy:
|
||||
1) releases/latest -> tag_name
|
||||
2) tags?limit=1 -> first tag name
|
||||
3) fallback: read manifest.json from repo (version field)
|
||||
"""
|
||||
session = async_get_clientsession(self.hass)
|
||||
base = self._base_from_repo(pkg.repo)
|
||||
@@ -94,7 +101,8 @@ class BahmcloudStore:
|
||||
data = await resp.json()
|
||||
tag = data.get("tag_name")
|
||||
html_url = data.get("html_url")
|
||||
return (str(tag) if tag else None, str(html_url) if html_url else None)
|
||||
if tag:
|
||||
return (str(tag), str(html_url) if html_url else None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -106,7 +114,21 @@ class BahmcloudStore:
|
||||
tags = await resp.json()
|
||||
if tags and isinstance(tags, list):
|
||||
name = tags[0].get("name")
|
||||
return (str(name) if name else None, None)
|
||||
if name:
|
||||
return (str(name), None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3) fallback: manifest.json version from repo
|
||||
try:
|
||||
manifest_url = self._raw_manifest_url(pkg.repo, pkg.branch, pkg.source_path)
|
||||
async with session.get(manifest_url, timeout=20) as resp:
|
||||
if resp.status == 200:
|
||||
text = await resp.text()
|
||||
data = json.loads(text)
|
||||
ver = data.get("version")
|
||||
if ver:
|
||||
return (str(ver), None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -198,6 +220,9 @@ class BahmcloudStore:
|
||||
shutil.rmtree(target)
|
||||
shutil.copytree(src, target)
|
||||
|
||||
# Nach Installation: Entities neu aufbauen (damit es als Update auftaucht)
|
||||
self.signal_entities_updated()
|
||||
|
||||
persistent_notification.async_create(
|
||||
self.hass,
|
||||
(
|
||||
@@ -228,7 +253,7 @@ class BahmcloudStore:
|
||||
async def register_http_views(self) -> None:
|
||||
"""Register HTTP views for static panel assets and JSON API."""
|
||||
self.hass.http.register_view(_StaticView())
|
||||
self.hass.http.register_view(_APIListView(self))
|
||||
self.hass.http.register_view(_APIView(self))
|
||||
|
||||
|
||||
class _StaticView(HomeAssistantView):
|
||||
@@ -236,12 +261,6 @@ class _StaticView(HomeAssistantView):
|
||||
IMPORTANT:
|
||||
Custom Panel JS modules are loaded WITHOUT Authorization headers.
|
||||
Therefore static panel assets must be publicly accessible (no auth).
|
||||
|
||||
Serves:
|
||||
/api/bahmcloud_store_static/index.html
|
||||
/api/bahmcloud_store_static/panel.js
|
||||
/api/bahmcloud_store_static/app.js
|
||||
/api/bahmcloud_store_static/styles.css
|
||||
"""
|
||||
requires_auth = False
|
||||
name = "bahmcloud_store_static"
|
||||
@@ -254,7 +273,6 @@ class _StaticView(HomeAssistantView):
|
||||
|
||||
f = (base / path).resolve()
|
||||
|
||||
# Prevent path traversal
|
||||
if not str(f).startswith(str(base)) or not f.exists() or not f.is_file():
|
||||
return web.Response(status=404, text="Not found")
|
||||
|
||||
@@ -269,10 +287,11 @@ class _StaticView(HomeAssistantView):
|
||||
return web.Response(body=f.read_bytes(), content_type="application/octet-stream")
|
||||
|
||||
|
||||
class _APIListView(HomeAssistantView):
|
||||
class _APIView(HomeAssistantView):
|
||||
"""
|
||||
Store API MUST stay protected.
|
||||
UI loads data via fetch() with HA auth handled by frontend.
|
||||
Auth-protected API:
|
||||
GET /api/bahmcloud_store -> list packages
|
||||
POST /api/bahmcloud_store {op:...} -> install/update a package
|
||||
"""
|
||||
requires_auth = True
|
||||
name = "bahmcloud_store_api"
|
||||
@@ -300,3 +319,20 @@ class _APIListView(HomeAssistantView):
|
||||
}
|
||||
)
|
||||
return self.json({"packages": items, "store_url": self.store.config.store_url})
|
||||
|
||||
async def post(self, request):
|
||||
data = await request.json()
|
||||
op = data.get("op")
|
||||
package_id = data.get("package_id")
|
||||
|
||||
if op not in ("install", "update"):
|
||||
return self.json({"error": "unknown op"}, status_code=400)
|
||||
if not package_id:
|
||||
return self.json({"error": "package_id missing"}, status_code=400)
|
||||
|
||||
pkg = self.store.packages.get(package_id)
|
||||
if not pkg:
|
||||
return self.json({"error": "unknown package_id"}, status_code=404)
|
||||
|
||||
await self.store.install_from_zip(pkg)
|
||||
return self.json({"ok": True})
|
||||
|
||||
Reference in New Issue
Block a user