fix: collision names must survive the sanitiser the content view applies

unique_filename produced 'manual (2).pdf'; HouseplanContentView sanitises the
name in the REQUEST too, turning ' (2)' into '_2_', so the file was written and
then 404'd. The same pattern was already in files/migrate, so a rebind that hit
a name collision has been producing dead links. Both use the shared helper now,
with '-2', which round-trips sanitize_filename — asserted.
This commit is contained in:
Matysh
2026-07-28 16:16:07 +03:00
parent 4418312b0b
commit a49b5e6d2e
7 changed files with 35 additions and 20 deletions
+1 -1
View File
@@ -690,7 +690,7 @@ async def test_upload_never_overwrites_an_existing_attachment(
names = sorted(
n for n in await hass.async_add_executor_job(os.listdir, folder) if n.startswith("manual")
)
assert names == ["manual (2).pdf", "manual.pdf"]
assert names == ["manual-2.pdf", "manual.pdf"]
got = await http.get(first.replace(CONTENT_URL, CONTENT_URL))
assert await got.read() == b"ONE", "the first file is untouched"
+10 -4
View File
@@ -392,14 +392,20 @@ def test_unique_filename_never_returns_a_taken_name(tmp_path):
d.mkdir()
assert unique_filename(d, "manual.pdf") == "manual.pdf"
(d / "manual.pdf").write_bytes(b"x")
assert unique_filename(d, "manual.pdf") == "manual (2).pdf"
(d / "manual (2).pdf").write_bytes(b"x")
assert unique_filename(d, "manual.pdf") == "manual (3).pdf"
assert unique_filename(d, "manual.pdf") == "manual-2.pdf"
(d / "manual-2.pdf").write_bytes(b"x")
assert unique_filename(d, "manual.pdf") == "manual-3.pdf"
# no extension, and a name that needs sanitising
(d / "readme").write_bytes(b"x")
assert unique_filename(d, "readme") == "readme (2)"
assert unique_filename(d, "readme") == "readme-2"
assert unique_filename(d, "../../etc/passwd") == "passwd"
# A generated name has to survive the sanitiser the CONTENT VIEW applies to
# the request, or the file is written and then never served: " (2)" became
# "_2_" and every collision-renamed attachment 404'd.
for candidate in ("manual-2.pdf", "manual-3.pdf", "readme-2"):
assert v.sanitize_filename(candidate) == candidate
def _acfg(*pairs):
return {"markers": [{"id": f"m{i}", "pdfs": [{"url": f"/api/houseplan/content/files/{p}"}]}