Skip to content

Commit d9a9eb5

Browse files
committed
refactor: lock related code, DONE!
1 parent 43364d8 commit d9a9eb5

9 files changed

Lines changed: 508 additions & 192 deletions

File tree

TODO

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
✔ 添加sztd支持 @done(25-11-14)
2424
✔ example 子项目代码更新 @high @done(25-12-28)
2525
✔ 测试覆盖 @done(25-12-28)
26-
HTTP 范围支持 @done(25-12-28)
26+
HTTP 范围支持
2727
✔ 断点续传支持开始和结尾,而不是只支持开始 @done(25-12-28)
2828
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests
2929
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Range_requests
@@ -42,6 +42,7 @@
4242
✔ 状态码支持 @done(25-12-27)
4343
✔ 206 Partial Content @done(25-12-27)
4444
✔ 416 Range Not Satisfiable @done(25-12-27)
45+
☐ 支持 multi-range
4546
✔ DAVTime 的 Web 时间格式时区应该是 GMT @high @done(25-12-29)
4647
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Last-Modified
4748
https://datatracker.ietf.org/doc/html/rfc9110.html#field.last-modified
@@ -55,8 +56,10 @@
5556
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/423
5657
✔ 完善 litmus 测试 @done(26-01-06)
5758
LOCK on unmapped url returned 200 not 201 (RFC4918:S7.3)
58-
☐ 梳理 DAVLock 的接口
59-
☐ create_propfind_response() multi-token support
59+
✔ 创建锁时, 支持 Depth.INFINITY 检查 @done(26-01-14)
60+
☐ 性能优化检查
61+
☐ 支持 multi-token
62+
- create_propfind_response()
6063
☐ 梳理 propfind/proppatch 等 XML 解析和生成代码 @low
6164
☐ 将 DAVPropertyBasicData.get_get_head_response_headers() 迁移到 DAVResponse
6265
☐ fileball连接会导致500错误

asgi_webdav/constants.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ def w3c(self) -> str:
381381
return self.data.isoformat(" ")
382382

383383
@cached_property
384-
def http_date(self) -> str: # TODO: fix timezone
384+
def http_date(self) -> str:
385385
# - https://datatracker.ietf.org/doc/html/rfc9110.html#section-5.6.7
386386
# 5.6.7. Date/Time Formats
387387
#
@@ -424,10 +424,10 @@ def __repr__(self) -> str:
424424
# --- common
425425
# - https://datatracker.ietf.org/doc/html/rfc4918#section-10.7
426426
# The timeout value for TimeType "Second" MUST NOT be greater than 2^32-1.
427-
DAVLockTimeoutMaxValue = 2**32 - 1 # TODO: move into config
427+
DAVLockTimeoutMaxValue = 2**32 - 1 # TODO: move into config ???
428428

429429

430-
class DAVLockScope(IntEnum):
430+
class DAVLockScope(Enum):
431431
"""
432432
https://tools.ietf.org/html/rfc4918
433433
14.13. lockscope XML Element
@@ -438,8 +438,8 @@ class DAVLockScope(IntEnum):
438438
<!ELEMENT lockscope (exclusive | shared) >
439439
"""
440440

441-
exclusive = auto()
442-
shared = auto()
441+
EXCLUSIVE = "exclusive"
442+
SHARED = "shared"
443443

444444

445445
# --- lock:request:header
@@ -489,7 +489,7 @@ class DAVLockObj:
489489

490490
# expire
491491
timeout: int
492-
_expire: float = field(init=False) # do not directly use it
492+
_expire: float = field(init=False)
493493

494494
@cached_property
495495
def hash_value(self) -> int:
@@ -507,7 +507,7 @@ def is_expired(self, now: float | None = None) -> bool:
507507

508508
return self._expire <= now
509509

510-
def check_path(self, path: DAVPath) -> bool:
510+
def is_locking_path(self, path: DAVPath) -> bool:
511511
"""Check if the path is locked by the current lock"""
512512
match self.depth:
513513
case DAVDepth.ZERO:
@@ -529,13 +529,13 @@ def __eq__(self, other: object) -> bool:
529529
def __repr__(self) -> str:
530530
s = ", ".join(
531531
[
532+
self.owner,
532533
self.path.raw,
533534
self.depth.__str__(),
535+
self.token.hex,
536+
self.scope.name,
534537
self.timeout.__str__(),
535538
self._expire.__str__(),
536-
self.scope.name,
537-
self.owner,
538-
self.token.hex,
539539
]
540540
)
541541
return f"DAVLockInfo({s})"
@@ -547,8 +547,8 @@ class DAVLockObjSet:
547547
_data: set[DAVLockObj]
548548

549549
@property
550-
def data(self) -> list[DAVLockObj]:
551-
return list(self._data)
550+
def data(self) -> set[DAVLockObj]:
551+
return self._data
552552

553553
def __contains__(self, lock_obj: DAVLockObj) -> bool:
554554
return lock_obj in self._data
@@ -562,6 +562,9 @@ def remove(self, lock_obj: DAVLockObj) -> None:
562562
def is_empty(self) -> bool:
563563
return not self._data
564564

565+
def __repr__(self) -> str:
566+
return f"DAVLockObjSet({self.lock_scope.name}, {len(self._data)})"
567+
565568

566569
# Property ---
567570
DAV_PROPERTY_BASIC_KEYS = {

asgi_webdav/lock.py

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -49,47 +49,108 @@ async def new(
4949
owner: str,
5050
path: DAVPath,
5151
depth: DAVDepth = DAVDepth.INFINITY,
52-
scope: DAVLockScope = DAVLockScope.exclusive,
52+
scope: DAVLockScope = DAVLockScope.EXCLUSIVE,
5353
timeout: int = DAVLockTimeoutMaxValue,
54+
lock_objs_of_path: list[DAVLockObj] | None = None,
5455
) -> DAVLockObj | None:
5556
"""return None if create lock failed"""
56-
# TODO: check with Depth.INFINITY
5757
async with self._asyncio_lock:
58-
lock_obj = DAVLockObj(
58+
new_lock_obj = DAVLockObj(
5959
owner=owner,
6060
path=path,
6161
depth=depth,
6262
token=uuid4(),
6363
scope=scope,
6464
timeout=timeout,
6565
)
66-
success = self._new_path2lock_obj_set(path, scope, lock_obj)
66+
success = self._new(new_lock_obj, lock_objs_of_path)
6767
if not success:
6868
return None
6969

70-
self._token2lock_obj[lock_obj.token] = lock_obj
71-
return lock_obj
70+
return new_lock_obj
7271

73-
def _new_path2lock_obj_set(
74-
self, path: DAVPath, lock_scope: DAVLockScope, lock_obj: DAVLockObj
72+
def _new(
73+
self,
74+
new_lock_obj: DAVLockObj,
75+
lock_objs_of_path: list[DAVLockObj] | None = None,
7576
) -> bool:
76-
lock_obj_set = self._path2lock_obj_set.get(path)
77-
if lock_obj_set is None:
78-
# new lock for path
79-
self._path2lock_obj_set[path] = DAVLockObjSet(lock_scope, {lock_obj})
77+
"""
78+
- 资源无锁
79+
- next
80+
- 资源有锁
81+
- 新锁的 scope 为 DAVLockScope.EXCLUSIVE
82+
- 锁定失败
83+
- 新锁的 scope 为 DAVLockScope.SHARED
84+
- 任意锁的 scope 为 DAVLockScope.EXCLUSIVE
85+
- 锁定失败
86+
- 所有锁的 scope 为 DAVLockScope.SHARED
87+
- next
88+
89+
- 新锁的 depth 为 DAVDepth.ZERO
90+
- Success
91+
- 新锁的 depth 为 DAVDepth.INFINITY
92+
- next
93+
94+
- 资源子目录无锁
95+
- Success
96+
- 资源子目录有锁
97+
- 新锁的 scope 为 DAVLockScope.EXCLUSIVE
98+
- Failed
99+
- 新锁的 scope 为 DAVLockScope.SHARED
100+
- 任意子目录锁的 scope 为 DAVLockScope.EXCLUSIVE
101+
- Failed
102+
- 所有子目录锁的 scope 为 DAVLockScope.SHARED
103+
- Success
104+
105+
resource has any lock:
106+
- resource's path has any lock
107+
- path's parent path's has any lock AND lock is DAVDepth.INFINITY
108+
"""
109+
if lock_objs_of_path is None:
110+
lock_objs_of_path = self._get_lock_objs_from_path(new_lock_obj.path)
111+
112+
if len(lock_objs_of_path) > 0:
113+
if new_lock_obj.scope == DAVLockScope.EXCLUSIVE:
114+
return False
115+
116+
for lock_obj in lock_objs_of_path:
117+
if lock_obj.scope == DAVLockScope.EXCLUSIVE:
118+
return False
119+
120+
if new_lock_obj.depth == DAVDepth.ZERO:
121+
self._new_just_do_it(new_lock_obj)
80122
return True
81123

82-
if lock_scope == DAVLockScope.exclusive:
83-
# can not lock some path with exclusive scope
84-
return False
124+
lock_objs_of_child_path = self._get_lock_objs_of_child_path_from_path(
125+
new_lock_obj.path
126+
)
127+
if len(lock_objs_of_child_path) == 0:
128+
self._new_just_do_it(new_lock_obj)
129+
return True
85130

86-
if lock_obj_set.lock_scope == DAVLockScope.exclusive:
87-
# can not lock some path with exclusive scope, even if the old lock's scope is shared
131+
if new_lock_obj.scope == DAVLockScope.EXCLUSIVE:
88132
return False
89133

90-
lock_obj_set.add(lock_obj)
134+
for lock_obj in lock_objs_of_child_path:
135+
if lock_obj.scope == DAVLockScope.EXCLUSIVE:
136+
return False
137+
138+
self._new_just_do_it(new_lock_obj)
91139
return True
92140

141+
def _new_just_do_it(self, new_lock_obj: DAVLockObj) -> None:
142+
lock_obj_set = self._path2lock_obj_set.get(new_lock_obj.path)
143+
if lock_obj_set is None:
144+
self._path2lock_obj_set[new_lock_obj.path] = DAVLockObjSet(
145+
new_lock_obj.scope, {new_lock_obj}
146+
)
147+
self._token2lock_obj[new_lock_obj.token] = new_lock_obj
148+
return
149+
150+
lock_obj_set.add(new_lock_obj)
151+
self._token2lock_obj[new_lock_obj.token] = new_lock_obj
152+
return
153+
93154
async def refresh(
94155
self, lock_obj: DAVLockObj, timeout: int | None = None
95156
) -> DAVLockObj:
@@ -114,14 +175,6 @@ async def release(self, token: UUID) -> bool:
114175
return self._release(lock_obj)
115176

116177
def _release(self, lock_obj: DAVLockObj) -> bool:
117-
success = self._release_path2lock_set(lock_obj)
118-
if not success:
119-
return False
120-
121-
self._token2lock_obj.pop(lock_obj.token)
122-
return True
123-
124-
def _release_path2lock_set(self, lock_obj: DAVLockObj) -> bool:
125178
path = lock_obj.path
126179

127180
if path not in self._path2lock_obj_set:
@@ -135,6 +188,7 @@ def _release_path2lock_set(self, lock_obj: DAVLockObj) -> bool:
135188
if lock_obj_set.is_empty():
136189
self._path2lock_obj_set.pop(path)
137190

191+
self._token2lock_obj.pop(lock_obj.token)
138192
return True
139193

140194
async def is_valid_lock_token(self, token: UUID, path: DAVPath) -> bool:
@@ -143,7 +197,7 @@ async def is_valid_lock_token(self, token: UUID, path: DAVPath) -> bool:
143197
if lock_obj is None:
144198
return False
145199

146-
if not lock_obj.check_path(path):
200+
if not lock_obj.is_locking_path(path):
147201
return False
148202

149203
if lock_obj.is_expired():
@@ -155,31 +209,49 @@ async def is_valid_lock_token(self, token: UUID, path: DAVPath) -> bool:
155209
return True
156210

157211
async def get_lock_objs_from_path(self, path: DAVPath) -> list[DAVLockObj]:
158-
now = time()
212+
async with self._asyncio_lock:
213+
return self._get_lock_objs_from_path(path)
159214

215+
def _get_lock_objs_from_path(self, path: DAVPath) -> list[DAVLockObj]:
216+
"""get lock_objs from path
217+
- path is locking by any lock
218+
- path's parent path's lock is DAVDepth.INFINITY
219+
"""
220+
now = time()
160221
lock_obj_expired = set()
161-
result: list[DAVLockObj] = list()
162-
async with self._asyncio_lock:
163-
for lock_path, lock_obj_set in self._path2lock_obj_set.items():
164-
if not lock_path.is_parent_of_or_is_self(path):
165-
continue
222+
lock_objs: list[DAVLockObj] = list()
166223

167-
for lock_obj in lock_obj_set.data:
168-
if lock_obj.is_expired(now=now):
169-
lock_obj_expired.add(lock_obj)
170-
continue
224+
for lock_path, lock_obj_set in self._path2lock_obj_set.items():
225+
if not lock_path.is_parent_of_or_is_self(path):
226+
continue
171227

172-
if not lock_obj.check_path(path):
173-
continue
228+
for lock_obj in list(lock_obj_set.data):
229+
if lock_obj.is_expired(now=now):
230+
lock_obj_expired.add(lock_obj)
231+
continue
174232

175-
result.append(lock_obj)
233+
if not lock_obj.is_locking_path(path):
234+
continue
176235

177-
for lock_obj in lock_obj_expired:
178-
if not self._release(lock_obj):
179-
raise DAVCodingError # pragma: no cover
236+
lock_objs.append(lock_obj)
180237

181-
return result
238+
for lock_obj in lock_obj_expired:
239+
if not self._release(lock_obj):
240+
raise DAVCodingError # pragma: no cover
241+
242+
return lock_objs
182243

183244
async def has_lock(self, path: DAVPath) -> bool:
184245
"""res path is locking by any lock"""
185-
return len(await self.get_lock_objs_from_path(path)) > 0
246+
async with self._asyncio_lock:
247+
return len(self._get_lock_objs_from_path(path)) > 0
248+
249+
def _get_lock_objs_of_child_path_from_path(self, path: DAVPath) -> list[DAVLockObj]:
250+
"""get lock_objs of child path from path"""
251+
result: list[DAVLockObj] = list()
252+
for lock_obj_set in self._path2lock_obj_set.values():
253+
for lock_obj in list(lock_obj_set.data):
254+
if path.is_parent_of(lock_obj.path):
255+
result.append(lock_obj)
256+
257+
return result

0 commit comments

Comments
 (0)