Restore blueprint backup restore support
This commit is contained in:
1
.idea/changes.md
generated
1
.idea/changes.md
generated
@@ -21,6 +21,7 @@
|
||||
- Bumped the integration version from `0.7.4` to `0.7.5` and added the `0.7.5` release entry to `CHANGELOG.md` for blueprint support and the documentation refresh.
|
||||
- Fixed blueprint update backups: blueprint file updates now create content backups before overwrite and can roll back copied blueprint files if installation fails.
|
||||
- Kept the no-restart behavior for blueprints, because blueprint deployment does not normally require a Home Assistant restart.
|
||||
- Restored blueprint backup/restore availability in the UI and backend: the restore button is visible again for blueprint installs, blueprint backups can be listed, and blueprint content can now be restored from backup without forcing a restart.
|
||||
|
||||
### Documented
|
||||
- Captured the verified project identity from the repository and README files: Bahmcloud Store is a Home Assistant custom integration intended to behave like a provider-neutral store for custom integrations, similar to HACS but broader than GitHub-only workflows.
|
||||
|
||||
@@ -1536,6 +1536,30 @@ class BCSCore:
|
||||
domains of the repository.
|
||||
"""
|
||||
inst = self.get_installed(repo_id) or {}
|
||||
install_type = str(inst.get("install_type") or "integration").strip() or "integration"
|
||||
if install_type == "blueprint":
|
||||
repo_root = self._backup_root / "_content" / self._backup_repo_key(repo_id)
|
||||
|
||||
def _list_content() -> list[str]:
|
||||
if not repo_root.exists() or not repo_root.is_dir():
|
||||
return []
|
||||
ids = [p.name for p in repo_root.iterdir() if p.is_dir()]
|
||||
ids.sort(reverse=True)
|
||||
return ids
|
||||
|
||||
ids = await self.hass.async_add_executor_job(_list_content)
|
||||
items: list[dict[str, Any]] = []
|
||||
for bid in ids[: self._backup_keep_per_domain]:
|
||||
label = self._format_backup_id(bid)
|
||||
meta = await self._read_content_backup_meta(repo_id, bid)
|
||||
ver = None
|
||||
if isinstance(meta, dict):
|
||||
ver = meta.get("installed_version") or meta.get("ref")
|
||||
if ver:
|
||||
label = f"{label} ({ver})"
|
||||
items.append({"id": bid, "label": label, "complete": True, "domains": [], "installed_version": str(ver) if ver else None})
|
||||
return items
|
||||
|
||||
domains = inst.get("domains") or []
|
||||
if not isinstance(domains, list) or not domains:
|
||||
return []
|
||||
@@ -1587,6 +1611,53 @@ class BCSCore:
|
||||
if not inst:
|
||||
raise BCSInstallError("Repository is not installed")
|
||||
|
||||
install_type = str(getattr(inst, "install_type", "integration") or "integration").strip() or "integration"
|
||||
if install_type == "blueprint":
|
||||
backup_path = self._backup_root / "_content" / self._backup_repo_key(repo_id) / backup_id
|
||||
if not backup_path.exists() or not backup_path.is_dir():
|
||||
raise BCSInstallError("Selected backup is not available")
|
||||
|
||||
installed_paths = [str(p).strip() for p in (getattr(inst, "installed_paths", None) or []) if str(p).strip()]
|
||||
|
||||
async with self._install_lock:
|
||||
_LOGGER.info("BCS restore started: repo_id=%s backup_id=%s type=blueprint", repo_id, backup_id)
|
||||
|
||||
try:
|
||||
await self._backup_paths(repo_id, installed_paths)
|
||||
except Exception:
|
||||
_LOGGER.debug("BCS pre-restore content backup failed for repo_id=%s", repo_id, exc_info=True)
|
||||
|
||||
await self._restore_paths_from_backup(backup_path, remove_targets=installed_paths)
|
||||
|
||||
restored_meta = await self._read_content_backup_meta(repo_id, backup_id)
|
||||
restored_version: str | None = None
|
||||
if isinstance(restored_meta, dict):
|
||||
rv = restored_meta.get("installed_version") or restored_meta.get("ref")
|
||||
if rv is not None and str(rv).strip():
|
||||
restored_version = str(rv).strip()
|
||||
if not restored_version:
|
||||
restored_version = f"restored:{backup_id}"
|
||||
|
||||
repo = self.get_repo(repo_id)
|
||||
repo_url = getattr(repo, "url", None) or ""
|
||||
|
||||
await self.storage.set_installed_repo(
|
||||
repo_id=repo_id,
|
||||
url=repo_url,
|
||||
domains=[],
|
||||
installed_version=restored_version,
|
||||
installed_manifest_version=None,
|
||||
ref=restored_version,
|
||||
install_type="blueprint",
|
||||
installed_paths=installed_paths,
|
||||
)
|
||||
|
||||
await self._refresh_installed_cache()
|
||||
self.signal_updated()
|
||||
|
||||
_LOGGER.info("BCS restore complete: repo_id=%s backup_id=%s type=blueprint", repo_id, backup_id)
|
||||
return {"ok": True, "repo_id": repo_id, "backup_id": backup_id, "domains": [], "installed_paths": installed_paths, "restored_version": restored_version, "restart_required": False}
|
||||
|
||||
domains = inst.get("domains") or []
|
||||
if not isinstance(domains, list) or not domains:
|
||||
raise BCSInstallError("No installed domains found")
|
||||
@@ -1683,6 +1754,17 @@ class BCSCore:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def _read_content_backup_meta(self, repo_id: str, backup_id: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
p = self._backup_root / "_content" / self._backup_repo_key(repo_id) / backup_id / BACKUP_META_FILENAME
|
||||
if not p.exists():
|
||||
return None
|
||||
txt = await self.hass.async_add_executor_job(p.read_text, 'utf-8')
|
||||
data = json.loads(txt)
|
||||
return data if isinstance(data, dict) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def _read_backup_manifest_version(self, domain: str, backup_id: str) -> str | None:
|
||||
"""Best-effort: read manifest.json version from a legacy backup (no metadata)."""
|
||||
|
||||
@@ -1379,9 +1379,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
const installBtn = `<button class="primary" id="btnInstall" ${installed || busy ? "disabled" : ""}>${busyInstall ? "Installing…" : installed ? "Installed" : "Install"}</button>`;
|
||||
const updateBtn = `<button class="primary" id="btnUpdate" ${!updateAvailable || busy ? "disabled" : ""}>${busyUpdate ? "Updating…" : updateAvailable ? "Update" : "Up to date"}</button>`;
|
||||
const uninstallBtn = `<button class="primary" id="btnUninstall" ${!installed || busy ? "disabled" : ""}>${busyUninstall ? "Uninstalling…" : "Uninstall"}</button>`;
|
||||
const restoreBtn = installType === "integration"
|
||||
? `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`
|
||||
: ``;
|
||||
const restoreBtn = `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`;
|
||||
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
|
||||
|
||||
const restartHint = this._restartRequired
|
||||
|
||||
Reference in New Issue
Block a user