mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
fix v1.41.2: uploaded files survive marker rebinding
- new WS houseplan/files/migrate moves /files/<oldId>/ to the new id (admin-only, sanitized ids, merge-safe, removes the empty old dir) - _saveMarker calls it and rewrites pdf urls (migratePdfUrls pure helper, +1 unit test, 112) when rebinding changes the id - field incident: the sauna heater manuals pointed at an orphaned old-id folder that got cleaned up; data restored by hand on the home instance (official Harvia PDFs re-downloaded) - TESTING/CHANGELOG same-commit
This commit is contained in:
@@ -32,6 +32,7 @@ def async_register(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_config_get)
|
||||
websocket_api.async_register_command(hass, ws_config_set)
|
||||
websocket_api.async_register_command(hass, ws_plan_set)
|
||||
websocket_api.async_register_command(hass, ws_files_migrate)
|
||||
|
||||
|
||||
def _runtime(hass: HomeAssistant, connection, msg_id: int) -> HouseplanData | None:
|
||||
@@ -108,6 +109,61 @@ async def ws_layout_update(hass: HomeAssistant, connection, msg: dict[str, Any])
|
||||
connection.send_result(msg["id"], {"ok": True})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "houseplan/files/migrate",
|
||||
vol.Required("from_id"): str,
|
||||
vol.Required("to_id"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_files_migrate(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
"""Move a marker's uploaded files to its new id (rebinding changes the id).
|
||||
|
||||
Without this the PDF urls keep pointing at the OLD id's folder, which then
|
||||
looks orphaned and is one cleanup away from deletion — the exact way the
|
||||
owner lost the sauna heater manuals (field incident, 2026-07-26).
|
||||
"""
|
||||
if not _check_write(hass, connection):
|
||||
connection.send_error(msg["id"], "unauthorized", "Only administrators may edit files")
|
||||
return
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from .const import FILES_DIR
|
||||
from .validation import sanitize_marker_id
|
||||
|
||||
src_id = sanitize_marker_id(msg["from_id"])
|
||||
dst_id = sanitize_marker_id(msg["to_id"])
|
||||
if not src_id or not dst_id or src_id == dst_id:
|
||||
connection.send_result(msg["id"], {"ok": True, "moved": 0})
|
||||
return
|
||||
base = Path(hass.config.path(FILES_DIR))
|
||||
src = base / src_id
|
||||
dst = base / dst_id
|
||||
|
||||
def _move() -> int:
|
||||
if not src.is_dir():
|
||||
return 0
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
n = 0
|
||||
for f in src.iterdir():
|
||||
if not f.is_file():
|
||||
continue
|
||||
target = dst / f.name
|
||||
if not target.exists():
|
||||
shutil.move(str(f), str(target))
|
||||
n += 1
|
||||
try:
|
||||
src.rmdir() # only when empty
|
||||
except OSError:
|
||||
pass
|
||||
return n
|
||||
|
||||
moved = await hass.async_add_executor_job(_move)
|
||||
connection.send_result(msg["id"], {"ok": True, "moved": moved})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "houseplan/layout/delete",
|
||||
|
||||
Reference in New Issue
Block a user