feat: add LOCK and UNLOCK methods to WebDAV API and improve path handling in existing methods

This commit is contained in:
shiyu
2026-05-02 16:30:56 +08:00
parent 1c216a7516
commit dcc8aa139e

View File

@@ -1,6 +1,7 @@
import base64 import base64
import hashlib import hashlib
import mimetypes import mimetypes
import uuid
from email.utils import formatdate from email.utils import formatdate
from urllib.parse import urlparse, unquote from urllib.parse import urlparse, unquote
from typing import Optional from typing import Optional
@@ -43,6 +44,8 @@ def _dav_headers(extra: Optional[dict] = None) -> dict:
"MKCOL", "MKCOL",
"MOVE", "MOVE",
"COPY", "COPY",
"LOCK",
"UNLOCK",
]), ]),
} }
if extra: if extra:
@@ -157,17 +160,19 @@ def _normalize_fs_path(path: str) -> str:
return unquote(full) return unquote(full)
@router.options("")
@router.options("/{path:path}") @router.options("/{path:path}")
@audit(action=AuditAction.READ, description="WebDAV: OPTIONS", user_kw="user") @audit(action=AuditAction.READ, description="WebDAV: OPTIONS", user_kw="user")
async def options_root(_request: Request, path: str = "", _enabled: None = Depends(_ensure_webdav_enabled)): async def options_root(_request: Request, path: str = "", _enabled: None = Depends(_ensure_webdav_enabled)):
return Response(status_code=200, headers=_dav_headers()) return Response(status_code=200, headers=_dav_headers())
@router.api_route("", methods=["PROPFIND"])
@router.api_route("/{path:path}", methods=["PROPFIND"]) @router.api_route("/{path:path}", methods=["PROPFIND"])
@audit(action=AuditAction.READ, description="WebDAV: PROPFIND", user_kw="user") @audit(action=AuditAction.READ, description="WebDAV: PROPFIND", user_kw="user")
async def propfind( async def propfind(
request: Request, request: Request,
path: str, path: str = "",
_enabled: None = Depends(_ensure_webdav_enabled), _enabled: None = Depends(_ensure_webdav_enabled),
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
@@ -280,29 +285,43 @@ async def dav_head(
return Response(status_code=200, headers=headers) return Response(status_code=200, headers=headers)
@router.api_route("", methods=["PUT"])
@router.api_route("/{path:path}", methods=["PUT"]) @router.api_route("/{path:path}", methods=["PUT"])
@audit(action=AuditAction.UPLOAD, description="WebDAV: PUT", user_kw="user") @audit(action=AuditAction.UPLOAD, description="WebDAV: PUT", user_kw="user")
async def dav_put( async def dav_put(
path: str,
request: Request, request: Request,
path: str = "",
_enabled: None = Depends(_ensure_webdav_enabled), _enabled: None = Depends(_ensure_webdav_enabled),
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE) await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
existed = True
try:
await VirtualFSService.stat_file(full_path)
except FileNotFoundError:
existed = False
except HTTPException as exc:
if exc.status_code == 404:
existed = False
else:
raise
async def body_iter(): async def body_iter():
async for chunk in request.stream(): async for chunk in request.stream():
if chunk: if chunk:
yield chunk yield chunk
size = await VirtualFSService.write_file_stream(full_path, body_iter(), overwrite=True)
return Response(status_code=201, headers=_dav_headers({"Content-Length": "0"})) await VirtualFSService.write_file_stream(full_path, body_iter(), overwrite=True)
return Response(status_code=204 if existed else 201, headers=_dav_headers({"Content-Length": "0"}))
@router.api_route("", methods=["DELETE"])
@router.api_route("/{path:path}", methods=["DELETE"]) @router.api_route("/{path:path}", methods=["DELETE"])
@audit(action=AuditAction.DELETE, description="WebDAV: DELETE", user_kw="user") @audit(action=AuditAction.DELETE, description="WebDAV: DELETE", user_kw="user")
async def dav_delete( async def dav_delete(
path: str,
_request: Request, _request: Request,
path: str = "",
_enabled: None = Depends(_ensure_webdav_enabled), _enabled: None = Depends(_ensure_webdav_enabled),
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
@@ -312,6 +331,58 @@ async def dav_delete(
return Response(status_code=204, headers=_dav_headers()) return Response(status_code=204, headers=_dav_headers())
@router.api_route("", methods=["LOCK"])
@router.api_route("/{path:path}", methods=["LOCK"])
@audit(action=AuditAction.UPDATE, description="WebDAV: LOCK", user_kw="user")
async def dav_lock(
path: str = "",
_request: Request = None,
_enabled: None = Depends(_ensure_webdav_enabled),
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
if full_path != "/":
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
token = f"opaquelocktoken:{uuid.uuid4()}"
ns = "{DAV:}"
prop = ET.Element(ns + "prop")
lockdiscovery = ET.SubElement(prop, ns + "lockdiscovery")
activelock = ET.SubElement(lockdiscovery, ns + "activelock")
locktype = ET.SubElement(activelock, ns + "locktype")
ET.SubElement(locktype, ns + "write")
lockscope = ET.SubElement(activelock, ns + "lockscope")
ET.SubElement(lockscope, ns + "exclusive")
depth = ET.SubElement(activelock, ns + "depth")
depth.text = "Infinity"
locktoken = ET.SubElement(activelock, ns + "locktoken")
href = ET.SubElement(locktoken, ns + "href")
href.text = token
xml = ET.tostring(prop, encoding="utf-8", xml_declaration=True)
return Response(
content=xml,
status_code=200,
media_type='application/xml; charset="utf-8"',
headers=_dav_headers({"Lock-Token": f"<{token}>"}),
)
@router.api_route("", methods=["UNLOCK"])
@router.api_route("/{path:path}", methods=["UNLOCK"])
@audit(action=AuditAction.UPDATE, description="WebDAV: UNLOCK", user_kw="user")
async def dav_unlock(
path: str = "",
_request: Request = None,
_enabled: None = Depends(_ensure_webdav_enabled),
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
if full_path != "/":
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
return Response(status_code=204, headers=_dav_headers())
@router.api_route("/{path:path}", methods=["MKCOL"]) @router.api_route("/{path:path}", methods=["MKCOL"])
@audit(action=AuditAction.CREATE, description="WebDAV: MKCOL", user_kw="user") @audit(action=AuditAction.CREATE, description="WebDAV: MKCOL", user_kw="user")
async def dav_mkcol( async def dav_mkcol(