diff --git a/README.md b/README.md index 728f7a5..4c3c3fc 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ The server provides integration with multiple Nextcloud apps, enabling LLMs to i | `nc_webdav_create_directory` | Create new directories | | `nc_webdav_delete_resource` | Delete files or directories | | `nc_webdav_move_resource` | Move or rename files and directories | +| `nc_webdav_copy_resource` | Copy files and directories | ## Available Resources @@ -126,6 +127,15 @@ await nc_webdav_move_resource("document.txt", "Archive/document.txt") # Move a directory await nc_webdav_move_resource("Projects/OldProject", "Projects/NewProject") + +# Copy a file +await nc_webdav_copy_resource("document.txt", "document_copy.txt") + +# Copy a file to another directory +await nc_webdav_copy_resource("document.txt", "Backup/document.txt") + +# Copy a directory +await nc_webdav_copy_resource("Projects/ProjectA", "Projects/ProjectA_Backup") ``` ### Calendar Integration diff --git a/nextcloud_mcp_server/client/webdav.py b/nextcloud_mcp_server/client/webdav.py index 2f7e905..6907286 100644 --- a/nextcloud_mcp_server/client/webdav.py +++ b/nextcloud_mcp_server/client/webdav.py @@ -496,3 +496,77 @@ class WebDAVClient(BaseNextcloudClient): f"Unexpected error moving resource from '{source_path}' to '{destination_path}': {e}" ) raise e + + async def copy_resource( + self, source_path: str, destination_path: str, overwrite: bool = False + ) -> Dict[str, Any]: + """Copy a resource (file or directory) via WebDAV COPY. + + Args: + source_path: The path of the file or directory to copy + destination_path: The destination path for the copy + overwrite: Whether to overwrite the destination if it exists + + Returns: + Dict with status_code and optional message + """ + source_webdav_path = f"{self._get_webdav_base_path()}/{source_path.lstrip('/')}" + destination_webdav_path = ( + f"{self._get_webdav_base_path()}/{destination_path.lstrip('/')}" + ) + + # Ensure paths have consistent trailing slashes for directories + if source_path.endswith("/") and not destination_path.endswith("/"): + destination_webdav_path += "/" + elif not source_path.endswith("/") and destination_path.endswith("/"): + source_webdav_path += "/" + + logger.debug(f"Copying resource from '{source_path}' to '{destination_path}'") + + headers = { + "OCS-APIRequest": "true", + "Destination": destination_webdav_path, + "Overwrite": "T" if overwrite else "F", + } + + try: + response = await self._make_request( + "COPY", source_webdav_path, headers=headers + ) + response.raise_for_status() + + logger.debug( + f"Successfully copied resource from '{source_path}' to '{destination_path}'" + ) + return {"status_code": response.status_code} + + except HTTPStatusError as e: + if e.response.status_code == 404: + logger.debug(f"Source resource '{source_path}' not found") + return {"status_code": 404, "message": "Source resource not found"} + elif e.response.status_code == 412: + logger.debug( + f"Destination '{destination_path}' already exists and overwrite is false" + ) + return { + "status_code": 412, + "message": "Destination already exists and overwrite is false", + } + elif e.response.status_code == 409: + logger.debug( + f"Parent directory of destination '{destination_path}' doesn't exist" + ) + return { + "status_code": 409, + "message": "Parent directory of destination doesn't exist", + } + else: + logger.error( + f"HTTP error copying resource from '{source_path}' to '{destination_path}': {e}" + ) + raise e + except Exception as e: + logger.error( + f"Unexpected error copying resource from '{source_path}' to '{destination_path}': {e}" + ) + raise e diff --git a/nextcloud_mcp_server/models/webdav.py b/nextcloud_mcp_server/models/webdav.py index 336f035..c85e2a8 100644 --- a/nextcloud_mcp_server/models/webdav.py +++ b/nextcloud_mcp_server/models/webdav.py @@ -96,3 +96,13 @@ class MoveResourceResponse(StatusResponse): overwrite: bool = Field( description="Whether the destination was overwritten if it existed" ) + + +class CopyResourceResponse(StatusResponse): + """Response model for resource copy operations.""" + + source_path: str = Field(description="Original path of the resource") + destination_path: str = Field(description="Destination path for the copy") + overwrite: bool = Field( + description="Whether the destination was overwritten if it existed" + ) diff --git a/nextcloud_mcp_server/server/webdav.py b/nextcloud_mcp_server/server/webdav.py index 67640b2..6fa6db6 100644 --- a/nextcloud_mcp_server/server/webdav.py +++ b/nextcloud_mcp_server/server/webdav.py @@ -181,3 +181,35 @@ def configure_webdav_tools(mcp: FastMCP): return await client.webdav.move_resource( source_path, destination_path, overwrite ) + + @mcp.tool() + async def nc_webdav_copy_resource( + source_path: str, destination_path: str, ctx: Context, overwrite: bool = False + ): + """Copy a file or directory in NextCloud. + + Args: + source_path: Full path of the file or directory to copy + destination_path: Destination path for the copy + overwrite: Whether to overwrite the destination if it exists (default: False) + + Returns: + Dict with status_code indicating result (404 if source not found, 412 if destination exists and overwrite is False) + + Examples: + # Copy a file + await nc_webdav_copy_resource("document.txt", "document_copy.txt") + + # Copy a file to another directory + await nc_webdav_copy_resource("document.txt", "Backup/document.txt") + + # Copy a directory + await nc_webdav_copy_resource("Projects/ProjectA", "Projects/ProjectA_Backup") + + # Copy and overwrite if destination exists + await nc_webdav_copy_resource("document.txt", "Backup/document.txt", overwrite=True) + """ + client: NextcloudClient = ctx.request_context.lifespan_context.client + return await client.webdav.copy_resource( + source_path, destination_path, overwrite + )