mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
v1.46.5: audit of every automatic deletion
Owner's decision after the incident: a detached plan is never deleted, at any age. v1.46.4 gave it a month; this makes it permanent and, more importantly, writes the reasoning where the next change will trip over it — docs/SCOPE.md now carries the standing rule. The component may delete a file only when a user action says so. 'Nothing points at this any more' is not such an action, because the two errors are not symmetrical: wasted disk is visible, cheap and reversible; a deleted file is none of those. Went through every other automatic deletion with the same question. One more was wrong: houseplan/files/cleanup rmtree'd whatever folder the card named. A partial migration leaves urls pointing into it — files/migrate deliberately does not rewrite the ones it could not confirm — so those were live links to files being deleted; and a wrong or stale id from any client destroyed a live device's manuals. The server now reads the stored config under its lock and removes only what nothing references, keeping the rest and saying so. Also: a plan of a DELETED space now waits thirty days rather than an hour. Deleting a space is deliberate; an hour is a short window to notice a misclick. The rest came out clean: layout/delete and marker/room/space removal are all confirm-guarded user actions, upload temporaries are never user-visible, and dropping legacy 'segments' is a documented migration.
This commit is contained in:
@@ -1002,3 +1002,47 @@ async def test_sweep_and_a_config_write_do_not_race(
|
||||
assert await hass.async_add_executor_job(
|
||||
os.path.isfile, os.path.join(plans, referenced)
|
||||
), f"the accepted config points at {referenced}, which must exist"
|
||||
|
||||
|
||||
async def test_files_cleanup_keeps_referenced_files(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""v1.46.5: the client may say what it no longer needs, never what may go.
|
||||
|
||||
`files/cleanup` used to rmtree the folder it was handed. A partial migration
|
||||
leaves some urls pointing into that folder — the copy deliberately does not
|
||||
rewrite the ones it could not confirm — so those were live links to files
|
||||
being deleted. A wrong id from any client had the same effect on a device's
|
||||
manuals.
|
||||
"""
|
||||
import os
|
||||
|
||||
from custom_components.houseplan.const import FILES_DIR
|
||||
|
||||
await _setup(hass)
|
||||
client = await hass_ws_client(hass)
|
||||
folder = os.path.join(hass.config.path(FILES_DIR), "m7")
|
||||
|
||||
def _seed() -> None:
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
for n in ("kept.pdf", "orphan.pdf"):
|
||||
with open(os.path.join(folder, n), "wb") as fh:
|
||||
fh.write(b"x")
|
||||
|
||||
await hass.async_add_executor_job(_seed)
|
||||
|
||||
cfg = await _cfg([{"id": "s7", "plan_url": None}])
|
||||
cfg["markers"] = [
|
||||
{"id": "other", "binding": "virtual",
|
||||
"pdfs": [{"name": "k", "url": "/api/houseplan/content/files/m7/kept.pdf"}]}
|
||||
]
|
||||
assert (await _save(client, cfg, 0))["success"]
|
||||
|
||||
await client.send_json_auto_id({"type": "houseplan/files/cleanup", "marker_id": "m7"})
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"] and resp["result"] == {"ok": True, "removed": 1, "kept": 1}
|
||||
|
||||
assert await hass.async_add_executor_job(os.path.isfile, os.path.join(folder, "kept.pdf")), (
|
||||
"a file the configuration still references survives a cleanup of its folder"
|
||||
)
|
||||
assert not await hass.async_add_executor_job(os.path.isfile, os.path.join(folder, "orphan.pdf"))
|
||||
|
||||
Reference in New Issue
Block a user