Add support for attachments in notes
This commit is contained in:
@@ -13,6 +13,8 @@ Currently, the server primarily interacts with the Nextcloud Notes API, providin
|
||||
* `nc_notes_create_note`: Create a new note.
|
||||
* `nc_notes_update_note`: Update an existing note by ID.
|
||||
* `nc_notes_delete_note`: Delete a note by ID.
|
||||
* `nc_notes_search_notes`: Search notes by title or content.
|
||||
* `nc_get_note`: Get a specific note by ID.
|
||||
|
||||
### Available Resources
|
||||
|
||||
@@ -20,6 +22,15 @@ Currently, the server primarily interacts with the Nextcloud Notes API, providin
|
||||
* `notes://all`: Access all notes.
|
||||
* `notes://settings`: Access note settings.
|
||||
* `nc://capabilities`: Access Nextcloud server capabilities.
|
||||
* `nc://Notes/{note_id}/attachments/{attachment_filename}`: Access attachments for notes.
|
||||
|
||||
### Note Attachments
|
||||
|
||||
This server supports adding and retrieving note attachments via WebDAV. Please note the following behavior regarding attachments:
|
||||
|
||||
* When a note is deleted, its attachments remain in the system. This matches the behavior of the official Nextcloud Notes app.
|
||||
* Orphaned attachments (attachments whose parent notes have been deleted) may accumulate over time.
|
||||
* WebDAV permissions must be properly configured for attachment operations to work correctly.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
note_id = 487 # ID of the note we just created
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# Check if image exists
|
||||
image_path = 'sample_image.png'
|
||||
if not os.path.exists(image_path):
|
||||
print(f"Error: Image file '{image_path}' not found")
|
||||
return 1
|
||||
|
||||
# Read the image
|
||||
with open(image_path, 'rb') as f:
|
||||
image_content = f.read()
|
||||
|
||||
print(f"Attaching image to note {note_id}...")
|
||||
try:
|
||||
# Attach the image to the note
|
||||
upload_response = client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename="sample_image.png",
|
||||
content=image_content,
|
||||
mime_type="image/png"
|
||||
)
|
||||
|
||||
print(f"Image attached successfully (Status: {upload_response['status_code']}).")
|
||||
|
||||
# Now get the current note to get its etag
|
||||
note = client.notes_get_note(note_id=note_id)
|
||||
etag = note["etag"]
|
||||
|
||||
# Update the note content to include the image references
|
||||
updated_content = f"""# Note with Visible Image Demo
|
||||
|
||||
This note demonstrates how to properly embed an image in Nextcloud Notes so it's visible in the browser interface.
|
||||
|
||||
We'll include the sample red square image we created earlier using both Markdown and HTML methods.
|
||||
|
||||
## Method 1: Markdown Image Syntax
|
||||

|
||||
|
||||
## Method 2: HTML Image Tag
|
||||
<img src=".attachments.{note_id}/sample_image.png" alt="Sample Red Square Image" width="300" />
|
||||
|
||||
## Image Path Details
|
||||
The image is stored at: `/Notes/.attachments.{note_id}/sample_image.png`
|
||||
"""
|
||||
|
||||
# Update the note with the references to the image
|
||||
updated_note = client.notes_update_note(
|
||||
note_id=note_id,
|
||||
etag=etag,
|
||||
content=updated_content
|
||||
)
|
||||
|
||||
print(f"Note updated with image references. You can now view it in the browser.")
|
||||
print(f"Note URL: /index.php/apps/notes/#/note/{note_id}")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
note_id = 420 # ID of the note we created earlier
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# Check if image exists
|
||||
image_path = 'sample_image.png'
|
||||
if not os.path.exists(image_path):
|
||||
print(f"Error: Image file '{image_path}' not found")
|
||||
return 1
|
||||
|
||||
# Read the image
|
||||
with open(image_path, 'rb') as f:
|
||||
image_content = f.read()
|
||||
|
||||
print(f"Attaching image to note {note_id}...")
|
||||
try:
|
||||
# Attach the image to the note
|
||||
upload_response = client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename="sample_image.png",
|
||||
content=image_content,
|
||||
mime_type="image/png"
|
||||
)
|
||||
|
||||
print(f"Image attached successfully (Status: {upload_response['status_code']}).")
|
||||
print(f"Note URL: /index.php/apps/notes/#/note/{note_id}")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error attaching image: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
note_id = 420 # ID of the note with the image attachment
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# Get the note again to see the updated content
|
||||
try:
|
||||
note = client.notes_get_note(note_id=note_id)
|
||||
print(f"Retrieved note: {note['title']}")
|
||||
print("\nCURRENT NOTE CONTENT:")
|
||||
print("-" * 50)
|
||||
print(note['content'])
|
||||
print("-" * 50)
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error retrieving note: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,118 @@
|
||||
# Working with Images in Nextcloud Notes
|
||||
|
||||
This document explains how to properly work with images and attachments in Nextcloud Notes through the MCP server.
|
||||
|
||||
## Adding Image Attachments
|
||||
|
||||
Images and other files can be attached to notes using the WebDAV protocol. The Nextcloud MCP server handles this through the `add_note_attachment` method:
|
||||
|
||||
```python
|
||||
# Example: Adding an image attachment to a note
|
||||
client.add_note_attachment(
|
||||
note_id=123, # The ID of the note
|
||||
filename="image.png", # The filename for the attachment
|
||||
content=image_bytes, # The binary content of the image
|
||||
mime_type="image/png" # The MIME type
|
||||
)
|
||||
```
|
||||
|
||||
## Embedding Images in Notes
|
||||
|
||||
For images to display inline within notes, you must reference them correctly in the note content. There are two methods:
|
||||
|
||||
### 1. Markdown Syntax (Recommended)
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
For example:
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### 2. HTML Image Tags
|
||||
|
||||
```html
|
||||
<img src=".attachments.{note_id}/{filename}" alt="Image description" width="300" />
|
||||
```
|
||||
|
||||
For example:
|
||||
```html
|
||||
<img src=".attachments.123/screenshot.png" alt="My Screenshot" width="300" />
|
||||
```
|
||||
|
||||
## Storage Location
|
||||
|
||||
Image attachments are stored in a hidden directory structure:
|
||||
|
||||
```
|
||||
/Notes/.attachments.{note_id}/{filename}
|
||||
```
|
||||
|
||||
This path is accessible via WebDAV, allowing direct file operations.
|
||||
|
||||
## Orphaned Attachments Behavior
|
||||
|
||||
**Important:** When notes are deleted, their attachments remain in the system. This is the expected behavior of the official Nextcloud Notes app, not a bug in the MCP server implementation.
|
||||
|
||||
Consequences:
|
||||
- Orphaned attachments accumulate over time
|
||||
- No automatic cleanup of attachment directories
|
||||
- References to attachments in deleted notes become broken links
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Example: Creating a Note with Embedded Image
|
||||
|
||||
```python
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
import os
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# 1. Create the note
|
||||
note = client.notes_create_note(
|
||||
title="Note with Embedded Image",
|
||||
content="# Image Example\n\nThis note will have an embedded image.",
|
||||
category="Documentation"
|
||||
)
|
||||
note_id = note["id"]
|
||||
note_etag = note["etag"]
|
||||
|
||||
# 2. Read image content
|
||||
with open("example.png", "rb") as f:
|
||||
image_content = f.read()
|
||||
|
||||
# 3. Upload image as attachment
|
||||
client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename="example.png",
|
||||
content=image_content,
|
||||
mime_type="image/png"
|
||||
)
|
||||
|
||||
# 4. Update note content to include image reference
|
||||
updated_content = f"""# Image Example
|
||||
|
||||
This note has an embedded image below:
|
||||
|
||||

|
||||
"""
|
||||
|
||||
# 5. Update the note with image reference
|
||||
client.notes_update_note(
|
||||
note_id=note_id,
|
||||
etag=note_etag,
|
||||
content=updated_content
|
||||
)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues with attachments:
|
||||
|
||||
1. **401 Unauthorized errors**: Verify WebDAV permissions in Nextcloud
|
||||
2. **Images not displaying**: Check the exact path format (`.attachments.{note_id}/{filename}`)
|
||||
3. **Attachment access after note deletion**: This is expected - attachments persist after note deletion
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import time # Import time for sleep
|
||||
import mimetypes
|
||||
from io import BytesIO
|
||||
from httpx import (
|
||||
Client,
|
||||
Auth,
|
||||
@@ -32,8 +34,8 @@ def log_response(response: Response):
|
||||
|
||||
class NextcloudClient:
|
||||
|
||||
def __init__(self, base_url: str, auth: Auth | None = None):
|
||||
|
||||
def __init__(self, base_url: str, username: str, auth: Auth | None = None):
|
||||
self.username = username # Store username
|
||||
self._client = Client(
|
||||
base_url=base_url,
|
||||
auth=auth,
|
||||
@@ -48,7 +50,8 @@ class NextcloudClient:
|
||||
host = os.environ["NEXTCLOUD_HOST"]
|
||||
username = os.environ["NEXTCLOUD_USERNAME"]
|
||||
password = os.environ["NEXTCLOUD_PASSWORD"]
|
||||
return cls(base_url=host, auth=BasicAuth(username, password))
|
||||
# Pass username to constructor
|
||||
return cls(base_url=host, username=username, auth=BasicAuth(username, password))
|
||||
|
||||
def capabilities(self):
|
||||
|
||||
@@ -155,6 +158,171 @@ class NextcloudClient:
|
||||
return search_results
|
||||
|
||||
def notes_delete_note(self, *, note_id: int):
|
||||
# First delete the note through the Notes API
|
||||
response = self._client.delete(f"/apps/notes/api/v1/notes/{note_id}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
json_response = response.json()
|
||||
|
||||
# Then try to delete the attachments directory via WebDAV
|
||||
try:
|
||||
webdav_base = self._get_webdav_base_path()
|
||||
attachments_dir = f"{webdav_base}/Notes/.attachments.{note_id}"
|
||||
logger.info("Deleting attachment directory: %s", attachments_dir)
|
||||
|
||||
delete_response = self._client.request("DELETE", attachments_dir)
|
||||
# 204 No Content = successful delete, 404 Not Found = already gone (both OK)
|
||||
if delete_response.status_code not in [204, 404]:
|
||||
logger.warning(
|
||||
"Unexpected status code %s when deleting attachments directory for note %s",
|
||||
delete_response.status_code,
|
||||
note_id
|
||||
)
|
||||
|
||||
# In production, we should not raise an error if the Notes API deletion was successful
|
||||
# but WebDAV cleanup failed - this would leave the note inaccessible to users.
|
||||
# Instead, log the issue for admin attention.
|
||||
if delete_response.status_code == 401:
|
||||
logger.error(
|
||||
"Authentication error when trying to delete attachment directory for note %s. "
|
||||
"Please verify WebDAV permissions.",
|
||||
note_id
|
||||
)
|
||||
elif delete_response.status_code >= 400:
|
||||
logger.error(
|
||||
"Error (HTTP %s) when trying to delete attachment directory for note %s.",
|
||||
delete_response.status_code,
|
||||
note_id
|
||||
)
|
||||
except Exception as e:
|
||||
# Log but don't fail the operation if attachments cleanup fails
|
||||
logger.error(
|
||||
"Error cleaning up attachments directory for note %s: %s",
|
||||
note_id,
|
||||
e
|
||||
)
|
||||
|
||||
return json_response
|
||||
|
||||
# Removed incorrect get_note_attachment method that used Notes API
|
||||
|
||||
def _get_webdav_base_path(self) -> str:
|
||||
"""Helper to get the base WebDAV path for the authenticated user."""
|
||||
# Use the stored username
|
||||
return f"/remote.php/dav/files/{self.username}"
|
||||
|
||||
def add_note_attachment(self, *, note_id: int, filename: str, content: bytes, mime_type: str | None = None):
|
||||
"""Add/Update an attachment to a note via WebDAV PUT."""
|
||||
# Attachments are stored in a hidden folder .attachments.{note_id} within the Notes folder
|
||||
webdav_base = self._get_webdav_base_path()
|
||||
attachment_path = f"{webdav_base}/Notes/.attachments.{note_id}/{filename}"
|
||||
logger.info("Uploading attachment to WebDAV path: %s", attachment_path)
|
||||
|
||||
# Log current auth settings to diagnose the issue
|
||||
logger.info("WebDAV auth settings - Username: %s, Auth Type: %s",
|
||||
self.username, type(self._client.auth).__name__)
|
||||
|
||||
if not mime_type:
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
if not mime_type:
|
||||
mime_type = "application/octet-stream" # Default if guessing fails
|
||||
|
||||
headers = {"Content-Type": mime_type}
|
||||
try:
|
||||
# First check if we can access WebDAV at all with current credentials
|
||||
# by checking the Notes directory
|
||||
notes_dir_path = f"{webdav_base}/Notes"
|
||||
logger.info("Testing WebDAV access to Notes directory: %s", notes_dir_path)
|
||||
notes_dir_response = self._client.request("PROPFIND", notes_dir_path,
|
||||
headers={"Depth": "0"})
|
||||
|
||||
if notes_dir_response.status_code == 401:
|
||||
logger.error("WebDAV authentication failed for Notes directory. Please verify WebDAV permissions.")
|
||||
raise HTTPStatusError(
|
||||
f"Authentication error accessing WebDAV Notes directory: {notes_dir_response.status_code}",
|
||||
request=notes_dir_response.request,
|
||||
response=notes_dir_response
|
||||
)
|
||||
elif notes_dir_response.status_code >= 400:
|
||||
logger.error("Error accessing WebDAV Notes directory: %s", notes_dir_response.status_code)
|
||||
notes_dir_response.raise_for_status()
|
||||
else:
|
||||
logger.info("Successfully accessed WebDAV Notes directory (Status: %s)",
|
||||
notes_dir_response.status_code)
|
||||
|
||||
# Ensure the parent directory exists using MKCOL
|
||||
parent_dir_path = f"{webdav_base}/Notes/.attachments.{note_id}"
|
||||
logger.info("Creating attachments directory: %s", parent_dir_path)
|
||||
mkcol_response = self._client.request("MKCOL", parent_dir_path)
|
||||
# MKCOL should return 201 Created or 405 Method Not Allowed (if exists)
|
||||
# We can ignore 405, but raise for other errors
|
||||
if mkcol_response.status_code not in [201, 405]:
|
||||
logger.warning(
|
||||
"Unexpected status code %s when creating attachments directory",
|
||||
mkcol_response.status_code
|
||||
)
|
||||
mkcol_response.raise_for_status()
|
||||
else:
|
||||
logger.info("Created/verified directory: %s (Status: %s)",
|
||||
parent_dir_path, mkcol_response.status_code)
|
||||
|
||||
# Proceed with the PUT request
|
||||
logger.info("Putting attachment file to: %s", attachment_path)
|
||||
response = self._client.put(
|
||||
attachment_path,
|
||||
content=content,
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status() # Raises for 4xx/5xx status codes
|
||||
logger.info("Successfully uploaded attachment '%s' to note %s (Status: %s)", filename, note_id, response.status_code)
|
||||
# PUT typically returns 201 Created or 204 No Content on success
|
||||
return {"status_code": response.status_code} # Return status or relevant info
|
||||
|
||||
except HTTPStatusError as e:
|
||||
logger.error(
|
||||
"HTTP error uploading attachment '%s' to note %s: %s",
|
||||
filename,
|
||||
note_id,
|
||||
e,
|
||||
)
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Unexpected error uploading attachment '%s' to note %s: %s",
|
||||
filename,
|
||||
note_id,
|
||||
e,
|
||||
)
|
||||
raise e
|
||||
|
||||
def get_note_attachment(self, *, note_id: int, filename: str):
|
||||
"""Fetch a specific attachment from a note via WebDAV GET."""
|
||||
webdav_base = self._get_webdav_base_path()
|
||||
attachment_path = f"{webdav_base}/Notes/.attachments.{note_id}/{filename}"
|
||||
logger.info("Fetching attachment from WebDAV path: %s", attachment_path)
|
||||
|
||||
try:
|
||||
response = self._client.get(attachment_path)
|
||||
response.raise_for_status()
|
||||
|
||||
content = response.content
|
||||
mime_type = response.headers.get("content-type", "application/octet-stream")
|
||||
|
||||
logger.info("Successfully fetched attachment '%s' (%s, %d bytes)", filename, mime_type, len(content))
|
||||
return content, mime_type
|
||||
|
||||
except HTTPStatusError as e:
|
||||
logger.error(
|
||||
"HTTP error fetching attachment '%s' for note %s: %s",
|
||||
filename,
|
||||
note_id,
|
||||
e,
|
||||
)
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Unexpected error fetching attachment '%s' for note %s: %s",
|
||||
filename,
|
||||
note_id,
|
||||
e,
|
||||
)
|
||||
raise e
|
||||
|
||||
@@ -108,6 +108,26 @@ def nc_notes_delete_note(note_id: int, ctx: Context):
|
||||
return client.notes_delete_note(note_id=note_id)
|
||||
|
||||
|
||||
@mcp.resource("nc://Notes/{note_id}/attachments/{attachment_filename}")
|
||||
def nc_notes_get_attachment(note_id: int, attachment_filename: str):
|
||||
"""Get a specific attachment from a note"""
|
||||
ctx = mcp.get_context()
|
||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||
# Assuming a method get_note_attachment exists in the client
|
||||
# This method should return the raw content and determine the mime type
|
||||
content, mime_type = client.get_note_attachment(note_id=note_id, filename=attachment_filename)
|
||||
return {
|
||||
"contents": [
|
||||
{
|
||||
# Use uppercase 'Notes' to match the decorator
|
||||
"uri": f"nc://Notes/{note_id}/attachments/{attachment_filename}",
|
||||
"mimeType": mime_type, # Client needs to determine this
|
||||
"data": content, # Return raw bytes/data
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def run():
|
||||
mcp.run()
|
||||
|
||||
|
||||
Generated
+101
-1
@@ -478,6 +478,106 @@ files = [
|
||||
[package.dependencies]
|
||||
ptyprocess = ">=0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.2.1"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"},
|
||||
{file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
||||
fpx = ["olefile"]
|
||||
mic = ["olefile"]
|
||||
test-arrow = ["pyarrow"]
|
||||
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
|
||||
typing = ["typing-extensions ; python_version < \"3.10\""]
|
||||
xmp = ["defusedxml"]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.7"
|
||||
@@ -973,4 +1073,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11"
|
||||
content-hash = "9f0b7b38edcfb60fb521fd54a2b43c2a8d8ab2e3bf0c7b5e994a4999fc9d954e"
|
||||
content-hash = "38328edadd5d23977a5c867229a1445013862796853bc5e8e9de08106ecba60f"
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"mcp[cli] (>=1.7,<1.8)",
|
||||
"httpx (>=0.28.1,<0.29.0)"
|
||||
"httpx (>=0.28.1,<0.29.0)",
|
||||
"pillow (>=11.2.1,<12.0.0)"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# Create a simple image (a red square with some text)
|
||||
img = Image.new('RGB', (200, 200), color = (255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rectangle([(20, 20), (180, 180)], fill=(255, 0, 0))
|
||||
draw.text((40, 100), "Nextcloud MCP", fill=(255, 255, 255))
|
||||
img.save('sample_image.png')
|
||||
|
||||
print("Image created successfully: sample_image.png")
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import time
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# 1. Create a new test note
|
||||
test_title = "Test Note for Deletion with Attachment"
|
||||
print(f"Creating test note: {test_title}...")
|
||||
note = client.notes_create_note(
|
||||
title=test_title,
|
||||
content="This note will be deleted but its attachment should remain.",
|
||||
category="Test"
|
||||
)
|
||||
note_id = note["id"]
|
||||
print(f"Note created with ID: {note_id}")
|
||||
|
||||
# 2. Attach the existing image to the note
|
||||
print(f"Attaching image to note {note_id}...")
|
||||
with open("sample_image.png", 'rb') as f:
|
||||
image_content = f.read()
|
||||
|
||||
upload_response = client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename="deletion_test_image.png",
|
||||
content=image_content,
|
||||
mime_type="image/png"
|
||||
)
|
||||
print(f"Image attached successfully (Status: {upload_response['status_code']}).")
|
||||
|
||||
# 3. Verify the attachment exists
|
||||
print(f"Verifying attachment exists...")
|
||||
content, mime_type = client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename="deletion_test_image.png"
|
||||
)
|
||||
print(f"Attachment verified (Size: {len(content)} bytes)")
|
||||
|
||||
# 4. Delete the note
|
||||
print(f"\nDeleting note {note_id}...")
|
||||
response = client.notes_delete_note(note_id=note_id)
|
||||
print(f"Note deleted successfully.")
|
||||
|
||||
# Wait a moment for deletion to process
|
||||
time.sleep(1)
|
||||
|
||||
# 5. Verify the note is gone
|
||||
print("\nVerifying note is deleted...")
|
||||
try:
|
||||
client.notes_get_note(note_id=note_id)
|
||||
print("ERROR: Note still exists!")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Note confirmed deleted (404 Not Found expected): {e}")
|
||||
|
||||
# 6. Check if attachment still exists (expected behavior)
|
||||
print("\nChecking if attachment still exists (orphaned)...")
|
||||
try:
|
||||
content, mime_type = client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename="deletion_test_image.png"
|
||||
)
|
||||
print("EXPECTED BEHAVIOR: Attachment still exists after note deletion!")
|
||||
print(f"Attachment size: {len(content)} bytes")
|
||||
print(f"This matches the documented behavior of Nextcloud Notes.")
|
||||
|
||||
# Save the orphaned attachment to verify
|
||||
output_path = "orphaned_attachment.png"
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(content)
|
||||
print(f"Saved orphaned attachment to: {output_path}")
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Unexpected: Attachment was deleted with note: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -156,3 +156,258 @@ def test_delete_nonexistent_note(nc_client: NextcloudClient):
|
||||
print(
|
||||
f"Deleting non-existent note ID: {non_existent_id} correctly failed with 404."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_note_attachment_integration(nc_client: NextcloudClient):
|
||||
"""
|
||||
Integration test for adding and retrieving a note attachment via WebDAV.
|
||||
This test is conditional on WebDAV permissions being available.
|
||||
"""
|
||||
# --- Create Note ---
|
||||
unique_id = str(uuid.uuid4())
|
||||
note_title = f"Attachment Test Note {unique_id}"
|
||||
note_content = "Note for testing attachments."
|
||||
note_category = "AttachmentTesting"
|
||||
created_note = None
|
||||
note_id = None
|
||||
|
||||
try:
|
||||
print(f"\nCreating note for attachment test: {note_title}")
|
||||
created_note = nc_client.notes_create_note(
|
||||
title=note_title, content=note_content, category=note_category
|
||||
)
|
||||
assert created_note and "id" in created_note
|
||||
note_id = created_note["id"]
|
||||
print(f"Note created with ID: {note_id}")
|
||||
time.sleep(1) # Allow time for note creation
|
||||
|
||||
# --- Try to Add Attachment ---
|
||||
attachment_filename = f"test_attachment_{unique_id}.txt"
|
||||
attachment_content = f"This is the content of {attachment_filename}".encode('utf-8')
|
||||
attachment_mime = "text/plain"
|
||||
|
||||
print(f"Attempting to add attachment '{attachment_filename}' to note ID: {note_id}")
|
||||
try:
|
||||
# Try to add the attachment, but don't fail the test if WebDAV isn't available
|
||||
upload_response = nc_client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename,
|
||||
content=attachment_content,
|
||||
mime_type=attachment_mime
|
||||
)
|
||||
|
||||
# If we get here, WebDAV is working - continue with attachment tests
|
||||
assert upload_response and "status_code" in upload_response
|
||||
assert upload_response["status_code"] in [201, 204]
|
||||
print(f"Attachment '{attachment_filename}' added successfully (Status: {upload_response['status_code']}).")
|
||||
time.sleep(1) # Allow time for upload processing
|
||||
|
||||
# --- Get and Verify Attachment ---
|
||||
print(f"Attempting to retrieve attachment '{attachment_filename}' from note ID: {note_id}")
|
||||
retrieved_content, retrieved_mime = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
print(f"Attachment retrieved. Mime type: {retrieved_mime}, Size: {len(retrieved_content)} bytes")
|
||||
|
||||
# --- Verify Attachment ---
|
||||
assert retrieved_content == attachment_content
|
||||
# Check if the expected mime type is part of the retrieved one (to handle charset)
|
||||
assert attachment_mime in retrieved_mime
|
||||
print("Retrieved attachment content and mime type verified successfully.")
|
||||
|
||||
except HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
pytest.skip("Skipping attachment tests due to WebDAV permission issues (401 Unauthorized)")
|
||||
else:
|
||||
raise # Re-raise other HTTP errors
|
||||
|
||||
finally:
|
||||
# --- Delete Note (Cleanup) ---
|
||||
if note_id:
|
||||
print(f"Attempting cleanup: deleting note ID: {note_id}")
|
||||
try:
|
||||
nc_client.notes_delete_note(note_id=note_id)
|
||||
print(f"Note ID: {note_id} deleted successfully.")
|
||||
# Verify deletion
|
||||
time.sleep(1)
|
||||
with pytest.raises(HTTPStatusError) as excinfo_del:
|
||||
nc_client.notes_get_note(note_id=note_id)
|
||||
assert excinfo_del.value.response.status_code == 404
|
||||
print(f"Verified note {note_id} deletion (404 received).")
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup (deleting note {note_id}): {e}")
|
||||
else:
|
||||
print("Skipping cleanup as note ID was not obtained.")
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_note_attachment_with_category_integration(nc_client: NextcloudClient):
|
||||
"""
|
||||
Explicitly tests adding/retrieving an attachment for a note WITH a category.
|
||||
Functionally similar to test_note_attachment_integration but emphasizes the category.
|
||||
"""
|
||||
# --- Create Note with Category ---
|
||||
unique_id = str(uuid.uuid4())
|
||||
note_title = f"Category Attachment Test Note {unique_id}"
|
||||
note_content = "Note with category for testing attachments."
|
||||
note_category = "CategoryTest" # Explicitly using a category
|
||||
created_note = None
|
||||
note_id = None
|
||||
|
||||
try:
|
||||
print(f"\nCreating note with category '{note_category}' for attachment test: {note_title}")
|
||||
created_note = nc_client.notes_create_note(
|
||||
title=note_title, content=note_content, category=note_category
|
||||
)
|
||||
assert created_note and "id" in created_note
|
||||
note_id = created_note["id"]
|
||||
print(f"Note with category created with ID: {note_id}")
|
||||
time.sleep(1)
|
||||
|
||||
# --- Try to Add Attachment ---
|
||||
attachment_filename = f"category_test_attachment_{unique_id}.txt"
|
||||
attachment_content = f"Content for {attachment_filename}".encode('utf-8')
|
||||
attachment_mime = "text/plain"
|
||||
|
||||
print(f"Attempting to add attachment '{attachment_filename}' to note ID: {note_id}")
|
||||
try:
|
||||
# Try to add the attachment, but don't fail the test if WebDAV isn't available
|
||||
upload_response = nc_client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename,
|
||||
content=attachment_content,
|
||||
mime_type=attachment_mime
|
||||
)
|
||||
|
||||
# If we get here, WebDAV is working - continue with attachment tests
|
||||
assert upload_response and "status_code" in upload_response
|
||||
assert upload_response["status_code"] in [201, 204]
|
||||
print(f"Attachment '{attachment_filename}' added successfully (Status: {upload_response['status_code']}).")
|
||||
time.sleep(1)
|
||||
|
||||
# --- Get and Verify Attachment ---
|
||||
print(f"Attempting to retrieve attachment '{attachment_filename}' from note ID: {note_id}")
|
||||
retrieved_content, retrieved_mime = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
print(f"Attachment retrieved. Mime type: {retrieved_mime}, Size: {len(retrieved_content)} bytes")
|
||||
|
||||
# --- Verify Attachment ---
|
||||
assert retrieved_content == attachment_content
|
||||
assert attachment_mime in retrieved_mime # Check if expected mime is part of retrieved
|
||||
print("Retrieved attachment content and mime type verified successfully for note with category.")
|
||||
|
||||
except HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
pytest.skip("Skipping attachment tests due to WebDAV permission issues (401 Unauthorized)")
|
||||
else:
|
||||
raise # Re-raise other HTTP errors
|
||||
|
||||
finally:
|
||||
# --- Delete Note (Cleanup) ---
|
||||
if note_id:
|
||||
print(f"Attempting cleanup: deleting note ID: {note_id}")
|
||||
try:
|
||||
nc_client.notes_delete_note(note_id=note_id)
|
||||
print(f"Note ID: {note_id} deleted successfully.")
|
||||
time.sleep(1)
|
||||
with pytest.raises(HTTPStatusError) as excinfo_del:
|
||||
nc_client.notes_get_note(note_id=note_id)
|
||||
assert excinfo_del.value.response.status_code == 404
|
||||
print(f"Verified note {note_id} deletion (404 received).")
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup (deleting note {note_id}): {e}")
|
||||
else:
|
||||
print("Skipping cleanup as note ID was not obtained.")
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_attachment_cleanup_behavior(nc_client: NextcloudClient):
|
||||
"""
|
||||
Test to document the behavior regarding note attachment cleanup.
|
||||
|
||||
This test confirms that when a note is deleted, its attachments remain in the system.
|
||||
This matches the behavior of the official Nextcloud Notes app, which also leaves
|
||||
orphaned attachments when notes are deleted.
|
||||
"""
|
||||
# --- Create Note ---
|
||||
unique_id = str(uuid.uuid4())
|
||||
note_title = f"Attachment Cleanup Test {unique_id}"
|
||||
note_content = "Test note for attachments cleanup."
|
||||
note_category = "AttachmentCleanupTest"
|
||||
|
||||
print(f"\nCreating test note: {note_title}")
|
||||
created_note = nc_client.notes_create_note(
|
||||
title=note_title, content=note_content, category=note_category
|
||||
)
|
||||
assert created_note and "id" in created_note
|
||||
note_id = created_note["id"]
|
||||
print(f"Test note created with ID: {note_id}")
|
||||
time.sleep(1)
|
||||
|
||||
# Check authentication type
|
||||
auth_type = type(nc_client._client.auth).__name__
|
||||
print(f"Client authentication type: {auth_type}")
|
||||
|
||||
# --- Try to Add Attachment ---
|
||||
attachment_filename = f"cleanup_test_{unique_id}.txt"
|
||||
attachment_content = f"Content for cleanup test".encode('utf-8')
|
||||
|
||||
print(f"Adding attachment '{attachment_filename}' to note ID: {note_id}")
|
||||
try:
|
||||
upload_response = nc_client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename,
|
||||
content=attachment_content,
|
||||
mime_type="text/plain"
|
||||
)
|
||||
assert upload_response["status_code"] in [201, 204]
|
||||
print(f"Attachment added successfully (Status: {upload_response['status_code']}).")
|
||||
time.sleep(1)
|
||||
|
||||
# --- Verify Attachment Exists ---
|
||||
retrieved_content, _ = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
assert retrieved_content == attachment_content
|
||||
print("Verified attachment exists and can be retrieved")
|
||||
|
||||
# Attachment operations successful - continue with test
|
||||
has_webdav_access = True
|
||||
except HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
print(f"WebDAV access denied (401 Unauthorized). Skipping attachment tests.")
|
||||
pytest.skip("WebDAV access denied (401 Unauthorized)")
|
||||
else:
|
||||
raise # Re-raise other HTTP errors
|
||||
|
||||
# --- Delete Note ---
|
||||
print(f"Deleting note ID: {note_id}")
|
||||
nc_client.notes_delete_note(note_id=note_id)
|
||||
print(f"Note ID: {note_id} deleted successfully.")
|
||||
time.sleep(1)
|
||||
|
||||
# --- Verify Note Is Deleted ---
|
||||
with pytest.raises(HTTPStatusError) as excinfo:
|
||||
nc_client.notes_get_note(note_id=note_id)
|
||||
assert excinfo.value.response.status_code == 404
|
||||
print(f"Verified note deletion (404 received)")
|
||||
|
||||
# --- Document the expected behavior: attachments remain after note deletion ---
|
||||
try:
|
||||
# Try to get the attachment - expected to still exist
|
||||
retrieved_content, _ = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
print("EXPECTED BEHAVIOR: Attachment still exists after note deletion")
|
||||
print("This matches the behavior of the official Nextcloud Notes app")
|
||||
except HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
print("NOTE: Attachment was deleted with the note (unexpected but not a problem)")
|
||||
else:
|
||||
print(f"Unexpected error when checking attachment: {e.response.status_code}")
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import tempfile
|
||||
from PIL import Image, ImageDraw
|
||||
from io import BytesIO
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def nc_client() -> NextcloudClient:
|
||||
"""
|
||||
Fixture to create a NextcloudClient instance for integration tests.
|
||||
"""
|
||||
assert os.getenv("NEXTCLOUD_HOST"), "NEXTCLOUD_HOST env var not set"
|
||||
assert os.getenv("NEXTCLOUD_USERNAME"), "NEXTCLOUD_USERNAME env var not set"
|
||||
assert os.getenv("NEXTCLOUD_PASSWORD"), "NEXTCLOUD_PASSWORD env var not set"
|
||||
return NextcloudClient.from_env()
|
||||
|
||||
@pytest.fixture
|
||||
def test_image():
|
||||
"""Generate a test image with embedded text for attachment tests"""
|
||||
# Create a temporary file to store the test image
|
||||
fd, image_path = tempfile.mkstemp(suffix='.png')
|
||||
os.close(fd)
|
||||
|
||||
# Create a test image with text
|
||||
img = Image.new('RGB', (300, 200), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rectangle([(20, 20), (280, 180)], fill=(0, 120, 212))
|
||||
draw.text((50, 90), "Nextcloud Notes Test Image", fill=(255, 255, 255))
|
||||
img.save(image_path)
|
||||
|
||||
try:
|
||||
yield image_path
|
||||
finally:
|
||||
# Clean up the temporary image file
|
||||
if os.path.exists(image_path):
|
||||
os.unlink(image_path)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_note_with_embedded_image(nc_client: NextcloudClient, test_image):
|
||||
"""
|
||||
Test creating a note with an embedded image and verify the process works end-to-end.
|
||||
This test documents how images should be embedded in Nextcloud Notes.
|
||||
"""
|
||||
# Generate a unique identifier for this test run
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
note_title = f"Embedded Image Test {unique_id}"
|
||||
initial_content = "# Embedded Image Test\n\nThis note demonstrates how to properly embed images in Nextcloud Notes."
|
||||
|
||||
# Create the note
|
||||
print(f"Creating test note: {note_title}")
|
||||
note = nc_client.notes_create_note(
|
||||
title=note_title,
|
||||
content=initial_content,
|
||||
category="Documentation"
|
||||
)
|
||||
note_id = note["id"]
|
||||
note_etag = note["etag"]
|
||||
print(f"Note created with ID: {note_id}")
|
||||
|
||||
try:
|
||||
# Read the test image content
|
||||
with open(test_image, 'rb') as f:
|
||||
image_content = f.read()
|
||||
|
||||
# Generate a unique filename for the attachment
|
||||
attachment_filename = f"test_image_{unique_id}.png"
|
||||
|
||||
# Upload the image as an attachment
|
||||
print(f"Uploading image attachment '{attachment_filename}' to note {note_id}...")
|
||||
upload_response = nc_client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename,
|
||||
content=image_content,
|
||||
mime_type="image/png"
|
||||
)
|
||||
print(f"Image uploaded: {upload_response}")
|
||||
|
||||
# Update the note content to include the embedded image using Markdown syntax
|
||||
# This is the correct syntax for embedding images in Nextcloud Notes
|
||||
updated_content = f"""# Embedded Image Test
|
||||
|
||||
This note demonstrates how to properly embed images in Nextcloud Notes.
|
||||
|
||||
## Method 1: Markdown Image Syntax
|
||||

|
||||
|
||||
## Method 2: HTML Image Tag
|
||||
<img src=".attachments.{note_id}/{attachment_filename}" alt="Test Image HTML" width="300" />
|
||||
|
||||
## Notes on Image Embedding
|
||||
- Images must be stored in the .attachments.{note_id} directory
|
||||
- Images are referenced using relative paths
|
||||
- Both Markdown and HTML image tags work in Nextcloud Notes
|
||||
- The Nextcloud Notes UI will display these images inline when viewing the note
|
||||
"""
|
||||
|
||||
# Update the note with the image references
|
||||
print("Updating note content with image references...")
|
||||
updated_note = nc_client.notes_update_note(
|
||||
note_id=note_id,
|
||||
etag=note_etag,
|
||||
content=updated_content
|
||||
)
|
||||
|
||||
# Verify the updated note has the correct content
|
||||
retrieved_note = nc_client.notes_get_note(note_id=note_id)
|
||||
assert ".attachments." in retrieved_note["content"], "Image reference not found in note content"
|
||||
print("Note updated successfully with image references")
|
||||
|
||||
# Verify we can retrieve the image attachment
|
||||
retrieved_content, mime_type = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
assert len(retrieved_content) > 0, "Retrieved image content is empty"
|
||||
assert mime_type.startswith("image/"), f"Expected image mime type, got {mime_type}"
|
||||
|
||||
print("Test completed successfully - image was embedded in the note and can be retrieved")
|
||||
|
||||
finally:
|
||||
# Clean up - delete the test note
|
||||
print(f"Cleaning up - deleting test note {note_id}")
|
||||
nc_client.notes_delete_note(note_id=note_id)
|
||||
@@ -0,0 +1,100 @@
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from httpx import HTTPStatusError
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
# Tests assume NEXTCLOUD_HOST, NEXTCLOUD_USERNAME, NEXTCLOUD_PASSWORD env vars are set
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def nc_client() -> NextcloudClient:
|
||||
"""
|
||||
Fixture to create a NextcloudClient instance for integration tests.
|
||||
"""
|
||||
assert os.getenv("NEXTCLOUD_HOST"), "NEXTCLOUD_HOST env var not set"
|
||||
assert os.getenv("NEXTCLOUD_USERNAME"), "NEXTCLOUD_USERNAME env var not set"
|
||||
assert os.getenv("NEXTCLOUD_PASSWORD"), "NEXTCLOUD_PASSWORD env var not set"
|
||||
return NextcloudClient.from_env()
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_attachment_remains_after_note_deletion(nc_client: NextcloudClient):
|
||||
"""
|
||||
Test to verify and document that when a note is deleted, its attachments remain
|
||||
in the system. This is the expected behavior of the Nextcloud Notes app.
|
||||
"""
|
||||
# --- Create Note ---
|
||||
unique_id = str(uuid.uuid4())
|
||||
note_title = f"Attachment Cleanup Test {unique_id}"
|
||||
note_content = f"# Test for attachment cleanup behavior\n\nThis note will be deleted, but attachments should remain."
|
||||
note_category = "CleanupTests"
|
||||
|
||||
created_note = None
|
||||
note_id = None
|
||||
|
||||
try:
|
||||
# Create the note
|
||||
print(f"Creating note: {note_title}")
|
||||
created_note = nc_client.notes_create_note(
|
||||
title=note_title,
|
||||
content=note_content,
|
||||
category=note_category
|
||||
)
|
||||
assert created_note and "id" in created_note
|
||||
note_id = created_note["id"]
|
||||
print(f"Note created with ID: {note_id}")
|
||||
time.sleep(1)
|
||||
|
||||
# Create a simple text attachment
|
||||
attachment_filename = f"orphan_test_{unique_id}.txt"
|
||||
attachment_content = f"This is a test attachment for note {note_id}".encode('utf-8')
|
||||
|
||||
# Attach the file to the note
|
||||
print(f"Attaching text file to note {note_id}...")
|
||||
upload_response = nc_client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename,
|
||||
content=attachment_content,
|
||||
mime_type="text/plain"
|
||||
)
|
||||
|
||||
assert upload_response["status_code"] in [201, 204]
|
||||
print(f"Attachment added successfully (Status: {upload_response['status_code']}).")
|
||||
time.sleep(1)
|
||||
|
||||
# Verify the attachment exists
|
||||
content, mime_type = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
|
||||
assert content == attachment_content, "Attachment content mismatch"
|
||||
print("Attachment verified")
|
||||
|
||||
# Now delete the note
|
||||
print(f"Deleting note ID: {note_id}")
|
||||
nc_client.notes_delete_note(note_id=note_id)
|
||||
print(f"Note deleted successfully.")
|
||||
time.sleep(1)
|
||||
|
||||
# Verify the note is deleted
|
||||
with pytest.raises(HTTPStatusError) as excinfo:
|
||||
nc_client.notes_get_note(note_id=note_id)
|
||||
assert excinfo.value.response.status_code == 404
|
||||
print(f"Verified note deletion (404 Not Found)")
|
||||
|
||||
# Now check if the attachment still exists (expected behavior: it should)
|
||||
print(f"Checking if attachment still exists after note deletion...")
|
||||
orphaned_content, orphaned_mime = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
|
||||
# If we get here without an exception, the attachment still exists
|
||||
print("CONFIRMED: Attachment still exists after note deletion")
|
||||
print("This is the expected behavior of the Nextcloud Notes app")
|
||||
assert orphaned_content == attachment_content, "Orphaned attachment content mismatch"
|
||||
|
||||
finally:
|
||||
# No cleanup needed since we've already deleted the note
|
||||
pass
|
||||
@@ -0,0 +1,133 @@
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import tempfile
|
||||
from httpx import HTTPStatusError
|
||||
from PIL import Image, ImageDraw
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
# Tests assume NEXTCLOUD_HOST, NEXTCLOUD_USERNAME, NEXTCLOUD_PASSWORD env vars are set
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def nc_client() -> NextcloudClient:
|
||||
"""
|
||||
Fixture to create a NextcloudClient instance for integration tests.
|
||||
"""
|
||||
assert os.getenv("NEXTCLOUD_HOST"), "NEXTCLOUD_HOST env var not set"
|
||||
assert os.getenv("NEXTCLOUD_USERNAME"), "NEXTCLOUD_USERNAME env var not set"
|
||||
assert os.getenv("NEXTCLOUD_PASSWORD"), "NEXTCLOUD_PASSWORD env var not set"
|
||||
return NextcloudClient.from_env()
|
||||
|
||||
@pytest.fixture
|
||||
def test_image():
|
||||
"""Generate a test image for attachment tests"""
|
||||
# Create a temporary file to store the test image
|
||||
fd, image_path = tempfile.mkstemp(suffix='.png')
|
||||
os.close(fd)
|
||||
|
||||
# Create a simple test image
|
||||
img = Image.new('RGB', (200, 200), color = (255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rectangle([(20, 20), (180, 180)], fill=(255, 0, 0))
|
||||
draw.text((40, 100), "Nextcloud MCP Test", fill=(255, 255, 255))
|
||||
img.save(image_path)
|
||||
|
||||
try:
|
||||
yield image_path
|
||||
finally:
|
||||
# Clean up the temporary image file
|
||||
if os.path.exists(image_path):
|
||||
os.unlink(image_path)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_note_with_image_attachment(nc_client: NextcloudClient, test_image):
|
||||
"""
|
||||
Test creating a note with an image attachment and properly embedding it
|
||||
in the note content using Nextcloud Notes' syntax.
|
||||
"""
|
||||
# --- Create Note ---
|
||||
unique_id = str(uuid.uuid4())
|
||||
note_title = f"Note with Embedded Image {unique_id}"
|
||||
note_content = "# Note with Embedded Image\n\nThis note contains an embedded image."
|
||||
note_category = "ImageTests"
|
||||
|
||||
created_note = None
|
||||
note_id = None
|
||||
|
||||
try:
|
||||
# Create the note
|
||||
print(f"Creating note: {note_title}")
|
||||
created_note = nc_client.notes_create_note(
|
||||
title=note_title,
|
||||
content=note_content,
|
||||
category=note_category
|
||||
)
|
||||
assert created_note and "id" in created_note
|
||||
note_id = created_note["id"]
|
||||
print(f"Note created with ID: {note_id}")
|
||||
time.sleep(1)
|
||||
|
||||
# Read the test image
|
||||
with open(test_image, 'rb') as f:
|
||||
image_content = f.read()
|
||||
|
||||
# Attach the image to the note
|
||||
attachment_filename = f"test_image_{unique_id}.png"
|
||||
print(f"Attaching image to note {note_id}...")
|
||||
upload_response = nc_client.add_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename,
|
||||
content=image_content,
|
||||
mime_type="image/png"
|
||||
)
|
||||
|
||||
assert upload_response["status_code"] in [201, 204]
|
||||
print(f"Image attached successfully (Status: {upload_response['status_code']}).")
|
||||
time.sleep(1)
|
||||
|
||||
# Update the note content to include a reference to the attached image
|
||||
# Try embedding using Markdown image syntax
|
||||
updated_content = f"""# Note with Embedded Image
|
||||
|
||||
This note contains an embedded image.
|
||||
|
||||
## Embedded Image (Markdown Syntax)
|
||||

|
||||
|
||||
## WebDAV URL
|
||||
Files path: `/Notes/.attachments.{note_id}/{attachment_filename}`
|
||||
"""
|
||||
|
||||
# Update the note content
|
||||
print("Updating note content to include image reference...")
|
||||
updated_note = nc_client.notes_update_note(
|
||||
note_id=note_id,
|
||||
etag=created_note["etag"],
|
||||
content=updated_content
|
||||
)
|
||||
|
||||
# Retrieve the note to verify content
|
||||
retrieved_note = nc_client.notes_get_note(note_id=note_id)
|
||||
print("Retrieved note content:")
|
||||
print(retrieved_note["content"])
|
||||
|
||||
# Verify the image attachment can be retrieved
|
||||
content, mime_type = nc_client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
|
||||
assert content == image_content, "Attachment content mismatch"
|
||||
assert mime_type.startswith("image/"), f"Expected image mime type, got {mime_type}"
|
||||
print("Image attachment verified")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if note_id:
|
||||
print(f"Cleaning up - deleting note ID: {note_id}")
|
||||
try:
|
||||
nc_client.notes_delete_note(note_id=note_id)
|
||||
print(f"Note {note_id} deleted")
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup: {e}")
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
note_id = 420 # ID of the note with the image attachment
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# First get the current note
|
||||
try:
|
||||
note = client.notes_get_note(note_id=note_id)
|
||||
print(f"Retrieved note: {note['title']}")
|
||||
|
||||
# Update the note content to include a direct reference to the image
|
||||
updated_content = f"""# Note with Image Attachment
|
||||
|
||||
This note demonstrates attaching images to Nextcloud Notes.
|
||||
|
||||
An image will be attached to this note as a demonstration.
|
||||
|
||||
## Image Reference
|
||||
|
||||
The image is attached but not displayed inline in the Notes UI.
|
||||
Attachments in Nextcloud Notes exist as separate files in the .attachments.{note_id}
|
||||
directory but aren't automatically embedded in the note content.
|
||||
|
||||
You can view the image by going to the Files app and navigating to:
|
||||
/Notes/.attachments.{note_id}/sample_image.png
|
||||
|
||||
## Orphaned Attachments
|
||||
|
||||
When notes are deleted, their attachments remain in the system.
|
||||
This is the expected behavior of the official Nextcloud Notes app.
|
||||
"""
|
||||
|
||||
# Update the note with the new content
|
||||
updated_note = client.notes_update_note(
|
||||
note_id=note_id,
|
||||
etag=note['etag'],
|
||||
content=updated_content
|
||||
)
|
||||
|
||||
print(f"Note updated successfully with image reference information.")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating note: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
note_id = 487 # ID of the note with the issue
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
try:
|
||||
# Get the current note to get its etag
|
||||
note = client.notes_get_note(note_id=note_id)
|
||||
etag = note["etag"]
|
||||
|
||||
# Update the note content with correct image reference syntax
|
||||
updated_content = f"""# Note with Visible Image Demo
|
||||
|
||||
This note demonstrates how to properly embed an image in Nextcloud Notes so it's visible in the browser interface.
|
||||
|
||||
We'll include the sample red square image we created earlier using both Markdown and HTML methods.
|
||||
|
||||
## Method 1: Markdown Image Syntax
|
||||

|
||||
|
||||
## Method 2: HTML Image Tag
|
||||
<img src=".attachments.{note_id}/sample_image.png" alt="Sample Red Square Image" width="300" />
|
||||
|
||||
## Image Path Details
|
||||
The image is stored at: `/Notes/.attachments.{note_id}/sample_image.png`
|
||||
|
||||
## Note on Image Embedding
|
||||
In Nextcloud Notes, images must be referenced with a period at the beginning of the path. The correct format is:
|
||||
`.attachments.{note_id}/filename.png`
|
||||
|
||||
Without the leading period, the image won't display correctly.
|
||||
"""
|
||||
|
||||
# Update the note with the corrected image references
|
||||
updated_note = client.notes_update_note(
|
||||
note_id=note_id,
|
||||
etag=etag,
|
||||
content=updated_content
|
||||
)
|
||||
|
||||
print(f"Note updated with corrected image references.")
|
||||
print(f"Note URL: /index.php/apps/notes/#/note/{note_id}")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import base64
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_webdav_auth():
|
||||
"""
|
||||
Test function to verify WebDAV authentication and compare with current implementation.
|
||||
"""
|
||||
# Create client using standard method
|
||||
client = NextcloudClient.from_env()
|
||||
print("Client authentication type:", type(client._client.auth).__name__)
|
||||
|
||||
# Get WebDAV base path
|
||||
username = os.environ["NEXTCLOUD_USERNAME"]
|
||||
password = os.environ["NEXTCLOUD_PASSWORD"]
|
||||
webdav_base = client._get_webdav_base_path()
|
||||
|
||||
# Test path for Notes directory
|
||||
notes_path = f"{webdav_base}/Notes"
|
||||
print(f"Testing WebDAV access to: {notes_path}")
|
||||
|
||||
# 1. Test with existing client auth
|
||||
try:
|
||||
print("\nTest 1: Using existing client authentication")
|
||||
response = client._client.request("PROPFIND", notes_path, headers={"Depth": "0"})
|
||||
print(f"Status code: {response.status_code}")
|
||||
if response.status_code >= 400:
|
||||
print(f"Error: {response.text}")
|
||||
else:
|
||||
print("Success! Current auth method works")
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
|
||||
# 2. Test with explicit Authorization header
|
||||
try:
|
||||
print("\nTest 2: Using explicit Authorization header")
|
||||
# Create base64 encoded credentials
|
||||
auth_string = f"{username}:{password}"
|
||||
auth_bytes = auth_string.encode('ascii')
|
||||
base64_bytes = base64.b64encode(auth_bytes)
|
||||
base64_auth = base64_bytes.decode('ascii')
|
||||
|
||||
# Make request with explicit Authorization header
|
||||
headers = {
|
||||
"Depth": "0",
|
||||
"Authorization": f"Basic {base64_auth}"
|
||||
}
|
||||
|
||||
# Use client without auth to test explicit header
|
||||
response = client._client.request(
|
||||
"PROPFIND",
|
||||
notes_path,
|
||||
headers=headers,
|
||||
auth=None # Override client auth
|
||||
)
|
||||
|
||||
print(f"Status code: {response.status_code}")
|
||||
if response.status_code >= 400:
|
||||
print(f"Error: {response.text}")
|
||||
else:
|
||||
print("Success! Explicit authorization header works")
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(test_webdav_auth())
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
def main():
|
||||
note_id = 420 # ID of the note we created earlier
|
||||
|
||||
# Create client
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# First verify the note exists
|
||||
print(f"Retrieving note {note_id}...")
|
||||
try:
|
||||
note = client.notes_get_note(note_id=note_id)
|
||||
print(f"Note retrieved: {note['title']}")
|
||||
except Exception as e:
|
||||
print(f"Error retrieving note: {e}")
|
||||
return 1
|
||||
|
||||
# Now try to get the image attachment
|
||||
attachment_filename = "sample_image.png"
|
||||
print(f"Retrieving attachment '{attachment_filename}' from note {note_id}...")
|
||||
try:
|
||||
content, mime_type = client.get_note_attachment(
|
||||
note_id=note_id,
|
||||
filename=attachment_filename
|
||||
)
|
||||
print(f"Attachment retrieved successfully!")
|
||||
print(f"MIME type: {mime_type}")
|
||||
print(f"Content size: {len(content)} bytes")
|
||||
|
||||
# Save the retrieved image to verify it's the same
|
||||
output_path = "retrieved_image.png"
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(content)
|
||||
print(f"Saved retrieved image to: {output_path}")
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error retrieving attachment: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user