fix(webdav): address PR #764 review round 4

- Guard against malformed PROPFIND responses where tag["id"] is None
  before calling get_files_by_tag (prevents
  <oc:systemtag>None</oc:systemtag> dispatch).
- Add OCS-APIRequest: true header to get_tag_by_name and
  get_files_by_tag to match every other PROPFIND/REPORT in the file —
  fixes a latent reverse-proxy compatibility hazard.
- Add test_copy_resource_blocks_excluded_source to mirror the
  existing move-source coverage; closes the asymmetric test gap.
- Add test_skips_tag_with_missing_id covering the new fail-open
  branch in _resolve_one_tag.
- Reword _resolve_one_tag docstring: "distinct slot" was misleading
  (tasks append rather than pre-allocate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-06 20:24:11 +02:00
co-authored by Claude Opus 4.7
parent 2ee4d03e3f
commit 56f01b3499
4 changed files with 63 additions and 7 deletions
+6 -2
View File
@@ -1135,7 +1135,11 @@ class WebDAVClient(BaseNextcloudClient):
response = await self._make_request(
"PROPFIND",
"/remote.php/dav/systemtags/",
headers={"Depth": "1", "Content-Type": "text/xml"},
headers={
"Depth": "1",
"Content-Type": "text/xml",
"OCS-APIRequest": "true",
},
content=propfind_body,
)
# Redundant after _make_request (which raises on non-2xx) but
@@ -1223,7 +1227,7 @@ class WebDAVClient(BaseNextcloudClient):
response = await self._make_request(
"REPORT",
f"{self._get_webdav_base_path()}/",
headers={"Content-Type": "text/xml"},
headers={"Content-Type": "text/xml", "OCS-APIRequest": "true"},
content=report_body,
)
# Redundant after _make_request (which raises on non-2xx) but
+14 -5
View File
@@ -46,11 +46,11 @@ async def _resolve_one_tag(
) -> None:
"""Resolve a single tag's paths and append them as a set to *results*.
Each task writes to a distinct slot in the shared listappend from
cooperative tasks is safe under anyio (single-threaded between
awaits) without an explicit lock. Swallows its own exceptions so a
failure for one tag does not abort the surrounding task group
(preserves fail-open per-tag semantics).
Each task appends its own set to the shared list; ``list.append`` is
atomic between cooperative yields under anyio (single-threaded
between awaits) so no explicit lock is needed. Swallows its own
exceptions so a failure for one tag does not abort the surrounding
task group (preserves fail-open per-tag semantics).
"""
try:
tag = await webdav.get_tag_by_name(tag_name)
@@ -67,6 +67,15 @@ async def _resolve_one_tag(
logger.debug("Excluded tag %r does not exist — skipping", tag_name)
return
if tag.get("id") is None:
# Malformed PROPFIND response: <oc:systemtag> entry without
# <oc:id/>. Skip rather than dispatch <oc:systemtag>None</oc:systemtag>.
logger.debug(
"Excluded tag %r has no id in PROPFIND response — skipping",
tag_name,
)
return
try:
files = await webdav.get_files_by_tag(tag["id"])
except Exception as e: