578de4d7d6
- Add Prometheus metrics for HTTP, MCP tools, Nextcloud API, OAuth, vector sync, and DB operations - Add OpenTelemetry distributed tracing with OTLP export - Add structured JSON logging with trace context correlation - Add ObservabilityMiddleware for automatic HTTP instrumentation - Add app_name attribute to all client classes for per-app metrics - Add configuration for metrics, tracing, and logging via environment variables - Add documentation in docs/observability.md - Fix graceful degradation when tracing is disabled (default state) - Fix uvicorn logging configuration to use observability formatters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
211 lines
6.8 KiB
Python
211 lines
6.8 KiB
Python
"""Nextcloud OCS Sharing API client for file/folder sharing operations."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from .base import BaseNextcloudClient, retry_on_429
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SharingClient(BaseNextcloudClient):
|
|
"""Client for Nextcloud OCS Sharing API operations."""
|
|
|
|
app_name = "sharing"
|
|
|
|
@retry_on_429
|
|
async def create_share(
|
|
self,
|
|
path: str,
|
|
share_with: str,
|
|
share_type: int = 0,
|
|
permissions: int = 1,
|
|
) -> dict[str, Any]:
|
|
"""Create a share for a file or folder.
|
|
|
|
Args:
|
|
path: Path to file/folder to share (relative to user's files)
|
|
share_with: Username (for user share) or group name (for group share)
|
|
share_type: Share type (0=user, 1=group, 3=public link)
|
|
permissions: Share permissions:
|
|
- 1 = read
|
|
- 2 = update
|
|
- 4 = create
|
|
- 8 = delete
|
|
- 16 = share
|
|
- 31 = all permissions
|
|
Common combinations: 1 (read-only), 3 (read+update), 15 (read+update+create+delete)
|
|
|
|
Returns:
|
|
Share data including share ID
|
|
|
|
Raises:
|
|
HTTPStatusError: If the request fails
|
|
"""
|
|
response = await self._client.post(
|
|
"/ocs/v2.php/apps/files_sharing/api/v1/shares",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
data={
|
|
"path": path,
|
|
"shareType": share_type,
|
|
"shareWith": share_with,
|
|
"permissions": permissions,
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# OCS API v2 uses HTTP-style status codes (200 for success)
|
|
# OCS API v1 used custom codes (100 for success)
|
|
ocs_status = data["ocs"]["meta"]["statuscode"]
|
|
if ocs_status not in (100, 200):
|
|
ocs_message = data["ocs"]["meta"].get("message", "Unknown error")
|
|
raise RuntimeError(f"OCS API error (code {ocs_status}): {ocs_message}")
|
|
|
|
share_data = data["ocs"]["data"]
|
|
|
|
# Handle case where data might be an empty list on error
|
|
if not share_data or (isinstance(share_data, list) and len(share_data) == 0):
|
|
ocs_message = data["ocs"]["meta"].get("message", "Unknown error")
|
|
raise RuntimeError(
|
|
f"Share creation failed: {ocs_message} (status {ocs_status})"
|
|
)
|
|
|
|
logger.info(
|
|
f"Created share {share_data['id']}: {path} -> {share_with} "
|
|
f"(type={share_type}, permissions={permissions})"
|
|
)
|
|
return share_data
|
|
|
|
@retry_on_429
|
|
async def delete_share(self, share_id: int) -> None:
|
|
"""Delete a share by its ID.
|
|
|
|
Args:
|
|
share_id: The share ID to delete
|
|
|
|
Raises:
|
|
HTTPStatusError: If the request fails
|
|
"""
|
|
response = await self._client.delete(
|
|
f"/ocs/v2.php/apps/files_sharing/api/v1/shares/{share_id}",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if data["ocs"]["meta"]["statuscode"] not in (100, 200):
|
|
raise RuntimeError(
|
|
f"OCS API error: {data['ocs']['meta'].get('message', 'Unknown error')}"
|
|
)
|
|
|
|
logger.info(f"Deleted share {share_id}")
|
|
|
|
@retry_on_429
|
|
async def get_share(self, share_id: int) -> dict[str, Any]:
|
|
"""Get information about a specific share.
|
|
|
|
Args:
|
|
share_id: The share ID
|
|
|
|
Returns:
|
|
Share data
|
|
|
|
Raises:
|
|
HTTPStatusError: If the request fails
|
|
"""
|
|
response = await self._client.get(
|
|
f"/ocs/v2.php/apps/files_sharing/api/v1/shares/{share_id}",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if data["ocs"]["meta"]["statuscode"] not in (100, 200):
|
|
raise RuntimeError(
|
|
f"OCS API error: {data['ocs']['meta'].get('message', 'Unknown error')}"
|
|
)
|
|
|
|
share_data = data["ocs"]["data"]
|
|
# The API returns a list with a single share, extract the first element
|
|
if isinstance(share_data, list) and len(share_data) > 0:
|
|
return share_data[0]
|
|
return share_data
|
|
|
|
@retry_on_429
|
|
async def list_shares(
|
|
self, path: str | None = None, shared_with_me: bool = False
|
|
) -> list[dict[str, Any]]:
|
|
"""List shares.
|
|
|
|
Args:
|
|
path: Optional path to filter shares for a specific file/folder
|
|
shared_with_me: If True, list shares shared with the current user
|
|
|
|
Returns:
|
|
List of share data
|
|
|
|
Raises:
|
|
HTTPStatusError: If the request fails
|
|
"""
|
|
params = {}
|
|
if path:
|
|
params["path"] = path
|
|
if shared_with_me:
|
|
params["shared_with_me"] = "true"
|
|
|
|
response = await self._client.get(
|
|
"/ocs/v2.php/apps/files_sharing/api/v1/shares",
|
|
params=params,
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if data["ocs"]["meta"]["statuscode"] not in (100, 200):
|
|
raise RuntimeError(
|
|
f"OCS API error: {data['ocs']['meta'].get('message', 'Unknown error')}"
|
|
)
|
|
|
|
# Handle both single share and list of shares
|
|
shares_data = data["ocs"]["data"]
|
|
if isinstance(shares_data, dict):
|
|
return [shares_data]
|
|
return shares_data if shares_data else []
|
|
|
|
@retry_on_429
|
|
async def update_share(
|
|
self, share_id: int, permissions: int | None = None
|
|
) -> dict[str, Any]:
|
|
"""Update a share's permissions.
|
|
|
|
Args:
|
|
share_id: The share ID to update
|
|
permissions: New permissions value (see create_share for values)
|
|
|
|
Returns:
|
|
Updated share data
|
|
|
|
Raises:
|
|
HTTPStatusError: If the request fails
|
|
"""
|
|
data = {}
|
|
if permissions is not None:
|
|
data["permissions"] = permissions
|
|
|
|
response = await self._client.put(
|
|
f"/ocs/v2.php/apps/files_sharing/api/v1/shares/{share_id}",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
data=data,
|
|
)
|
|
response.raise_for_status()
|
|
result = response.json()
|
|
|
|
if result["ocs"]["meta"]["statuscode"] not in (100, 200):
|
|
raise RuntimeError(
|
|
f"OCS API error: {result['ocs']['meta'].get('message', 'Unknown error')}"
|
|
)
|
|
|
|
logger.info(f"Updated share {share_id}")
|
|
return result["ocs"]["data"]
|