From a9492bfa1df74d850b314ad8eaf75dc7466c36bf Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:37:41 +0000 Subject: [PATCH] Optimize WebDAV responsiveness for WinSCP and other clients. This commit introduces several performance improvements to the WebDAV layer: - Added a 1024-item LRU cache for file descriptors in `DCMsgFDRepository` to minimize Discord API overhead. - Optimized `Folder.member()` and `Resource.__init__()` to reuse existing `DCFSFileRef` objects, eliminating redundant directory traversals. - Increased `PROPFIND` concurrency from 10 to 20 in `asgidav/reqres.py` to speed up large directory listings. These changes significantly improve the "smoothness" and responsiveness when browsing and accessing files via WebDAV. Co-authored-by: VulcanoSoftware <113239901+VulcanoSoftware@users.noreply.github.com> --- asgidav/reqres.py | 2 +- dcfs/app/webdav/folder.py | 10 +++--- dcfs/app/webdav/resource.py | 7 ++-- dcfs/core/repository/impl/fd/dc_msg.py | 48 +++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/asgidav/reqres.py b/asgidav/reqres.py index 730bc54..cd9f26e 100644 --- a/asgidav/reqres.py +++ b/asgidav/reqres.py @@ -84,7 +84,7 @@ async def _propstat(member: Member, prop_names: Tuple[PropertyName, ...]) -> Ele # Limit concurrent member lookups so large directories don't overwhelm # the event loop or the Discord API rate limits. -_MAX_CONCURRENT_MEMBERS = 10 +_MAX_CONCURRENT_MEMBERS = 20 async def _propfind_response( diff --git a/dcfs/app/webdav/folder.py b/dcfs/app/webdav/folder.py index eedcdbc..25e7174 100644 --- a/dcfs/app/webdav/folder.py +++ b/dcfs/app/webdav/folder.py @@ -17,8 +17,8 @@ def __init__(self, path: str, client: Client): self.__client = client self.__ops = Ops(client) self.__folder = client.dir_api.root if path == "/" else self.__ops.cd(path) - self.__sub_folders = frozenset([d.name for d in self.__folder.find_dirs()]) - self.__sub_files = frozenset([f.name for f in self.__folder.find_files()]) + self.__sub_folders = {d.name: d for d in self.__folder.find_dirs()} + self.__sub_files = {f.name: f for f in self.__folder.find_files()} @property def fs_cache(self) -> FSCache: @@ -28,15 +28,15 @@ async def display_name(self) -> str: return self.__folder.name async def member_names(self): - return self.__sub_folders.union(self.__sub_files) + return set(self.__sub_folders.keys()).union(self.__sub_files.keys()) async def member(self, path: str): path_parts = path.split("/", 1) if path_parts[0] == "": return self - if path_parts[0] in self.__sub_files: - return Resource(self._sub_path(path_parts[0]), self.__client) + if fr := self.__sub_files.get(path_parts[0]): + return Resource(self._sub_path(path_parts[0]), self.__client, fr=fr) if path_parts[0] in self.__sub_folders: if len(path_parts) > 1: diff --git a/dcfs/app/webdav/resource.py b/dcfs/app/webdav/resource.py index 0976d22..9b78f53 100644 --- a/dcfs/app/webdav/resource.py +++ b/dcfs/app/webdav/resource.py @@ -11,7 +11,7 @@ class Resource(_Resource): - def __init__(self, path: str, client: Client): + def __init__(self, path: str, client: Client, fr: Optional[DCFSFileRef] = None): super().__init__(f"/{client.name}{path}") self.__relative_path = path @@ -19,8 +19,9 @@ def __init__(self, path: str, client: Client): self.__fs_cache = gfc[client.name] self.__ops = Ops(client) - if not (fr := self.__ops.stat_file(path)): - raise TechnicalError(f"Resource {path} does not exist") + if fr is None: + if not (fr := self.__ops.stat_file(path)): + raise TechnicalError(f"Resource {path} does not exist") self.__fr: DCFSFileRef = fr self.__fd_value: Optional[DCFSFileDesc] = None diff --git a/dcfs/core/repository/impl/fd/dc_msg.py b/dcfs/core/repository/impl/fd/dc_msg.py index 3732fd6..4b3e5de 100644 --- a/dcfs/core/repository/impl/fd/dc_msg.py +++ b/dcfs/core/repository/impl/fd/dc_msg.py @@ -19,8 +19,36 @@ class DCMsgFDRepository(IFDRepository): - def __init__(self, message_api: MessageApi): + def __init__(self, message_api: MessageApi, cache_capacity: int = 1024): self._message_api = message_api + self._cache_capacity = cache_capacity + self._cache: dict[int, DCFSFileDesc] = {} + self._cache_order: list[int] = [] + + def _cache_put(self, message_id: int, fd: DCFSFileDesc) -> None: + if message_id in self._cache: + # Refresh position in LRU + self._cache_order.remove(message_id) + elif len(self._cache) >= self._cache_capacity: + # Evict oldest + evict = self._cache_order.pop(0) + self._cache.pop(evict, None) + + self._cache[message_id] = fd + self._cache_order.append(message_id) + + def _cache_get(self, message_id: int) -> Optional[DCFSFileDesc]: + if message_id in self._cache: + # Refresh position in LRU + self._cache_order.remove(message_id) + self._cache_order.append(message_id) + return self._cache[message_id] + return None + + def _cache_invalidate(self, message_id: int) -> None: + if message_id in self._cache: + self._cache.pop(message_id) + self._cache_order.remove(message_id) async def _send_with_retry(self, text: str) -> int: """Send a file descriptor with exponential backoff retry.""" @@ -81,15 +109,19 @@ async def save( self, fd: DCFSFileDesc, fr: Optional[DCFSFileRef] = None ) -> FDRepositoryResp: if fr is None: + message_id = await self._send_with_retry(fd.to_json()) + self._cache_put(message_id, fd) return FDRepositoryResp( - message_id=await self._send_with_retry(fd.to_json()), + message_id=message_id, fd=fd, ) + message_id = await self._edit_with_retry( + message_id=fr.message_id, text=fd.to_json() + ) + self._cache_put(message_id, fd) return FDRepositoryResp( - message_id=await self._edit_with_retry( - message_id=fr.message_id, text=fd.to_json() - ), + message_id=message_id, fd=fd, ) @@ -139,15 +171,21 @@ async def get( include_all_versions: bool = False, validate: bool = True, ) -> DCFSFileDesc: + if not validate and (cached := self._cache_get(fr.message_id)): + return cached + message = (await self._message_api.get_messages([fr.message_id]))[0] if not message: logging.error( f"File descriptor (message_id: {fr.message_id}) for {fr.name} not found" ) + self._cache_invalidate(fr.message_id) return DCFSFileDesc.empty(fr.name) fd = DCFSFileDesc.from_dict(json.loads(message.text), name=fr.name) + self._cache_put(fr.message_id, fd) + if not validate: return fd