feat: Add WebDAV resource copy functionality
This commit is contained in:
@@ -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_create_directory` | Create new directories |
|
||||||
| `nc_webdav_delete_resource` | Delete files or directories |
|
| `nc_webdav_delete_resource` | Delete files or directories |
|
||||||
| `nc_webdav_move_resource` | Move or rename files and directories |
|
| `nc_webdav_move_resource` | Move or rename files and directories |
|
||||||
|
| `nc_webdav_copy_resource` | Copy files and directories |
|
||||||
|
|
||||||
## Available Resources
|
## Available Resources
|
||||||
|
|
||||||
@@ -126,6 +127,15 @@ await nc_webdav_move_resource("document.txt", "Archive/document.txt")
|
|||||||
|
|
||||||
# Move a directory
|
# Move a directory
|
||||||
await nc_webdav_move_resource("Projects/OldProject", "Projects/NewProject")
|
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
|
### Calendar Integration
|
||||||
|
|||||||
@@ -496,3 +496,77 @@ class WebDAVClient(BaseNextcloudClient):
|
|||||||
f"Unexpected error moving resource from '{source_path}' to '{destination_path}': {e}"
|
f"Unexpected error moving resource from '{source_path}' to '{destination_path}': {e}"
|
||||||
)
|
)
|
||||||
raise 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
|
||||||
|
|||||||
@@ -96,3 +96,13 @@ class MoveResourceResponse(StatusResponse):
|
|||||||
overwrite: bool = Field(
|
overwrite: bool = Field(
|
||||||
description="Whether the destination was overwritten if it existed"
|
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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -181,3 +181,35 @@ def configure_webdav_tools(mcp: FastMCP):
|
|||||||
return await client.webdav.move_resource(
|
return await client.webdav.move_resource(
|
||||||
source_path, destination_path, overwrite
|
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
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user