Skip to content

Commit 59eefac

Browse files
committed
chore: update xmltodict to v1.0.2
1 parent ddc193e commit 59eefac

9 files changed

Lines changed: 261 additions & 52 deletions

File tree

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
repos:
22
- repo: https://github.com/asottile/pyupgrade
3-
rev: v3.19.1
3+
rev: v3.21.2
44
hooks:
55
- id: pyupgrade
66
args: [ --py310-plus ]
77
- repo: https://github.com/psf/black
8-
rev: 25.1.0
8+
rev: 25.12.0
99
hooks:
1010
- id: black
1111
args: [ "--target-version", "py310" ]
1212
- repo: https://github.com/pycqa/isort
13-
rev: 6.0.1
13+
rev: 7.0.0
1414
hooks:
1515
- id: isort
1616
name: isort (python)
1717
- repo: https://github.com/PyCQA/flake8
18-
rev: 7.2.0
18+
rev: 7.3.0
1919
hooks:
2020
- id: flake8

TODO

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@
5151
☐ 文档自动部署失效问题
5252
- 每年定期更新上游依赖
5353
☐ aiofiles
54-
xmltodict
54+
xmltodict v1.0.2 @done(25-12-09)
5555

5656
待实现:
5757
☐ 基于配置,检查依赖安装情况,以便在启动时告警
5858
☐ 兼容 Py3.14 @block dataclass-wizard 不兼容 py3.14
59-
☐ 更新 xmltodict 到最新版本
60-
这个上游库变化非常大, 之前版本与现在版本不兼容
6159
☐ 浏览器前台界面 @low
6260
☐ 更干净的目录过滤机制
6361
☐ 更好看的界面

asgi_webdav/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
class EnvConfig(EnvWizard):
3434
class _(EnvWizard.Meta):
3535
env_prefix = "WEBDAV_"
36+
env_file = True
3637

3738
username: str | None = None
3839
password: str | None = None

asgi_webdav/helpers.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from logging import getLogger
66
from mimetypes import guess_type as orig_guess_type
77
from pathlib import Path
8+
from typing import Any
89
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
910

1011
import aiofiles
@@ -147,13 +148,20 @@ def dav_dict2xml(data: dict) -> bytes:
147148
)
148149

149150

150-
def dav_xml2dict(data: bytes) -> dict | None:
151+
def get_dav_property_data_from_xml(data: bytes, propert_type: str) -> dict[str, Any]:
151152
try:
152153
result = xmltodict.parse(data, process_namespaces=True)
153154

154155
except (xmltodict.ParsingInterrupted, xml.parsers.expat.ExpatError) as e:
155-
logger.warning(f"parser XML failed, {e}, {data}")
156-
return None
156+
logger.warning(f"parser XML {propert_type} failed: {e}, xml: {data}")
157+
return {}
158+
159+
try:
160+
result = result[f"DAV::{propert_type}"]
161+
162+
except (ValueError, KeyError) as e:
163+
logger.warning(f"parser XML {propert_type} failed: {e}, xml: {data}")
164+
return {}
157165

158166
return result
159167

asgi_webdav/request.py

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import urllib.parse
33
from collections.abc import Callable
44
from dataclasses import dataclass, field
5+
from logging import getLogger
56
from pyexpat import ExpatError
67
from uuid import UUID
78

@@ -18,7 +19,15 @@
1819
DAVPropertyPatches,
1920
DAVUser,
2021
)
21-
from asgi_webdav.helpers import dav_xml2dict, receive_all_data_in_one_call
22+
from asgi_webdav.helpers import (
23+
get_dav_property_data_from_xml,
24+
receive_all_data_in_one_call,
25+
)
26+
27+
logger = getLogger(__name__)
28+
29+
30+
_XML_NAME_SPACE_TAG = "@xmlns"
2231

2332

2433
@dataclass
@@ -325,7 +334,6 @@ def _cut_ns_key(ns_key: str) -> tuple[str, str]:
325334
return ns_key[:index], ns_key[index + 1 :]
326335

327336
async def _parser_body_propfind(self) -> bool:
328-
self.body = await receive_all_data_in_one_call(self.receive)
329337
"""
330338
A client may choose not to submit a request body. An empty PROPFIND
331339
request body MUST be treated as if it were an 'allprop' request.
@@ -334,25 +342,24 @@ async def _parser_body_propfind(self) -> bool:
334342
# allprop
335343
return True
336344

337-
data = dav_xml2dict(self.body)
338-
if data is None:
345+
data = get_dav_property_data_from_xml(self.body, "propfind")
346+
if not data:
339347
return False
340348

341-
find_symbol = "DAV::propfind"
342-
if "propname" in data[find_symbol]:
349+
if "propname" in data:
343350
self.propfind_only_fetch_property_name = True
344351
return True
345352

346-
if "DAV::allprop" in data[find_symbol]:
353+
if "DAV::allprop" in data:
347354
return True
348355
else:
349356
self.propfind_fetch_all_property = False
350357

351-
if "DAV::prop" not in data[find_symbol]:
358+
if "DAV::prop" not in data:
352359
# TODO error
353360
return False
354361

355-
for ns_key in data[find_symbol]["DAV::prop"]:
362+
for ns_key in data["DAV::prop"]:
356363
ns, key = self._cut_ns_key(ns_key)
357364
if key in DAV_PROPERTY_BASIC_KEYS:
358365
self.propfind_basic_keys.add(key)
@@ -365,33 +372,42 @@ async def _parser_body_propfind(self) -> bool:
365372
return True
366373

367374
async def _parser_body_proppatch(self) -> bool:
368-
self.body = await receive_all_data_in_one_call(self.receive)
369-
data = dav_xml2dict(self.body)
370-
if data is None:
375+
data = get_dav_property_data_from_xml(self.body, "propertyupdate")
376+
if not data:
371377
return False
372378

373-
update_symbol = "DAV::propertyupdate"
374-
for action in data[update_symbol]:
375-
_, key = self._cut_ns_key(action)
376-
if key == "set":
379+
for action, action_data in data.items():
380+
if action == _XML_NAME_SPACE_TAG:
381+
continue
382+
383+
_, action_method = self._cut_ns_key(action)
384+
if action_method == "set":
377385
method = True
378386
else:
387+
# remove
379388
method = False
380389

381-
for item in data[update_symbol][action]:
382-
if isinstance(item, dict):
383-
ns_key, value = item["DAV::prop"].popitem()
384-
else:
385-
ns_key, value = data[update_symbol][action][item].popitem()
386-
if isinstance(value, dict):
387-
# value namespace: drop namespace info # TODO ???
388-
value, _ = value.popitem()
389-
_, value = self._cut_ns_key(value)
390-
# value = "<{} xmlns='{}'>".format(vns_key, vns_ns)
390+
if isinstance(action_data, dict):
391+
# 当 action 只有一条的时候, actions_data 是一个 dict, 需要转换为 list 以便后续处理
392+
action_data = [action_data]
391393

394+
for action_item in action_data:
395+
396+
ns_key, dav_prop_data = action_item["DAV::prop"].popitem()
392397
ns, key = self._cut_ns_key(ns_key)
398+
399+
# value = value.get("#text")
400+
value = None
401+
for prop_key, prop_value in dav_prop_data.items():
402+
if prop_key == _XML_NAME_SPACE_TAG:
403+
continue
404+
if prop_key == "#text":
405+
value = prop_value
406+
else:
407+
_, value = self._cut_ns_key(prop_key)
408+
393409
if not isinstance(value, str):
394-
value = str(value)
410+
value = str(value) # TODO: 可能不需要转换?
395411

396412
self.proppatch_entries.append(
397413
DAVPropertyPatches([DAVPropertyIdentity((ns, key)), value, method])
@@ -400,36 +416,39 @@ async def _parser_body_proppatch(self) -> bool:
400416
return True
401417

402418
async def _parser_body_lock(self) -> bool:
403-
self.body = await receive_all_data_in_one_call(self.receive)
404419
if len(self.body) == 0:
405420
# LOCK accept empty body
406421
return True
407422

408-
data = dav_xml2dict(self.body)
409-
if data is None:
423+
data = get_dav_property_data_from_xml(self.body, "lockinfo")
424+
if not data:
410425
return False
411426

412-
if "DAV::exclusive" in data["DAV::lockinfo"]["DAV::lockscope"]:
427+
if "DAV::exclusive" in data["DAV::lockscope"]:
413428
self.lock_scope = DAVLockScope.exclusive
414429
else:
415430
self.lock_scope = DAVLockScope.shared
416431

417-
lock_owner = data["DAV::lockinfo"]["DAV::owner"]
432+
lock_owner = data["DAV::owner"]
418433
self.lock_owner = str(lock_owner)
419434
return True
420435

421436
async def parser_body(self) -> bool:
422-
if self.method == DAVMethod.PROPFIND:
423-
self.body_is_parsed_success = await self._parser_body_propfind()
437+
match self.method:
438+
case DAVMethod.PROPFIND:
439+
self.body = await receive_all_data_in_one_call(self.receive)
440+
self.body_is_parsed_success = await self._parser_body_propfind()
424441

425-
elif self.method == DAVMethod.PROPPATCH:
426-
self.body_is_parsed_success = await self._parser_body_proppatch()
442+
case DAVMethod.PROPPATCH:
443+
self.body = await receive_all_data_in_one_call(self.receive)
444+
self.body_is_parsed_success = await self._parser_body_proppatch()
427445

428-
elif self.method == DAVMethod.LOCK:
429-
self.body_is_parsed_success = await self._parser_body_lock()
446+
case DAVMethod.LOCK:
447+
self.body = await receive_all_data_in_one_call(self.receive)
448+
self.body_is_parsed_success = await self._parser_body_lock()
430449

431-
else:
432-
self.body_is_parsed_success = False
450+
case _:
451+
self.body_is_parsed_success = False
433452

434453
return self.body_is_parsed_success
435454

docs/changelog.en.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Broken change:
66
- remove brotli support
77
- feat: support zstd compression in response
8+
- chore: update xmltodict to v1.0.2
89

910
## 1.6.1 - 20251101
1011

requirements.d/basic.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ aiofiles~=23.2.0
44
ASGIMiddlewareStaticFile~=0.6.0
55

66
# data
7-
xmltodict~=0.13.0
7+
xmltodict~=1.0.2
88

99
# config
10-
dataclass-wizard<1.0
10+
dataclass-wizard[dotenv]<1.0
1111
tomli; python_version < '3.11'
1212

1313
# misc

tests/test_helpers.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from asgi_webdav.helpers import (
77
detect_charset,
88
get_data_generator_from_content,
9+
get_dav_property_data_from_xml,
910
guess_type,
1011
is_browser_user_agent,
1112
paser_timezone_key,
@@ -123,6 +124,35 @@ async def test_func_get_data_generator_from_content():
123124
assert len(data_new) == 100
124125

125126

127+
def test_get_dav_property_data_from_xml():
128+
# all good
129+
assert get_dav_property_data_from_xml(
130+
data=b'<?xml version="1.0" encoding="utf-8" ?>\n<D:propertyupdate xmlns:D="DAV:"><D:set><D:prop><random xmlns="http://webdav.org/neon/litmus/">foobar</random></D:prop></D:set>\n</D:propertyupdate>\n',
131+
propert_type="propertyupdate",
132+
) == {
133+
"@xmlns": {"D": "DAV:"},
134+
"DAV::set": {
135+
"DAV::prop": {
136+
"http://webdav.org/neon/litmus/:random": {
137+
"@xmlns": {"": "http://webdav.org/neon/litmus/"},
138+
"#text": "foobar",
139+
}
140+
}
141+
},
142+
}
143+
144+
# bad
145+
assert get_dav_property_data_from_xml(data=b"", propert_type="propertyupdate") == {}
146+
147+
assert (
148+
get_dav_property_data_from_xml(
149+
data=b'<?xml version="1.0" encoding="utf-8" ?>\n<D:propertyupdate xmlns:D="DAV:"><D:set><D:prop><random xmlns="http://webdav.org/neon/litmus/">foobar</random></D:prop></D:set>\n</D:propertyupdate>\n',
150+
propert_type="bad",
151+
)
152+
== {}
153+
)
154+
155+
126156
def test_get_timezone_from_env():
127157
assert paser_timezone_key("Asia/Shanghai") == "Asia/Shanghai"
128158

0 commit comments

Comments
 (0)