| 1011 | |
| 1012 | |
| 1013 | class WebDAVFileStorage(FileStorageInterface): |
| 1014 | _instance: Optional["WebDAVFileStorage"] = None |
| 1015 | |
| 1016 | def __init__(self): |
| 1017 | if not hasattr(self, "_initialized"): |
| 1018 | self.base_url = settings.webdav_url.rstrip("/") + "/" |
| 1019 | self.auth = aiohttp.BasicAuth( |
| 1020 | login=settings.webdav_username, password=settings.webdav_password |
| 1021 | ) |
| 1022 | self._initialized = True |
| 1023 | |
| 1024 | def _build_url(self, path: str) -> str: |
| 1025 | encoded_path = quote(str(path.replace("\\", "/").lstrip("/")).lstrip("/")) |
| 1026 | return f"{self.base_url}{encoded_path}" |
| 1027 | |
| 1028 | async def _mkdir_p(self, directory_path: str): |
| 1029 | """递归创建目录(类似mkdir -p)""" |
| 1030 | path_obj = Path(unquote(directory_path)) |
| 1031 | current_path = "" |
| 1032 | |
| 1033 | async with aiohttp.ClientSession(auth=self.auth) as session: |
| 1034 | # 逐级检查目录是否存在 |
| 1035 | for part in path_obj.parts: |
| 1036 | current_path = str(Path(current_path) / part) |
| 1037 | url = self._build_url(current_path) |
| 1038 | |
| 1039 | # 检查目录是否存在 |
| 1040 | async with session.head(url) as resp: |
| 1041 | if resp.status == 404: |
| 1042 | # 创建目录 |
| 1043 | async with session.request("MKCOL", url) as mkcol_resp: |
| 1044 | if mkcol_resp.status not in (200, 201, 409): |
| 1045 | content = await mkcol_resp.text() |
| 1046 | raise HTTPException( |
| 1047 | status_code=mkcol_resp.status, |
| 1048 | detail=f"目录创建失败: {content[:200]}", |
| 1049 | ) |
| 1050 | |
| 1051 | async def _is_dir_empty(self, dir_path: str) -> bool: |
| 1052 | """检查目录是否为空""" |
| 1053 | url = self._build_url(dir_path) |
| 1054 | |
| 1055 | async with aiohttp.ClientSession(auth=self.auth) as session: |
| 1056 | async with session.request("PROPFIND", url, headers={"Depth": "1"}) as resp: |
| 1057 | if resp.status != 207: # 207 是 Multi-Status 响应 |
| 1058 | return False |
| 1059 | content = await resp.text() |
| 1060 | # 如果只有一个 response(当前目录),说明目录为空 |
| 1061 | return content.count("<D:response>") <= 1 |
| 1062 | |
| 1063 | async def _delete_empty_dirs(self, file_path: str, session: aiohttp.ClientSession): |
| 1064 | """递归删除空目录""" |
| 1065 | path_obj = Path(file_path) |
| 1066 | current_path = path_obj.parent |
| 1067 | |
| 1068 | while str(current_path) != ".": |
| 1069 | if not await self._is_dir_empty(str(current_path)): |
| 1070 | break |
nothing calls this directly
no outgoing calls
no test coverage detected