Files
kite/backend/storage/nextcloud_backend.py
T
curo1305 62128730e8 fix(12.1-01): restore asyncio import, bump version to 0.2.3, update docs
- Restore asyncio import removed when legacy list_folder was deleted (health_check
  still uses asyncio.to_thread); fixes test_nextcloud_connect_persists regression
- Bump backend/main.py and frontend/package.json version to 0.2.3
- Update CLAUDE.md: note Phase 12.1 Plan 01 complete, add normalize_nextcloud_url
  to shared module map, add NextcloudBackend no-override rule
- Update README.md: version 0.2.3, note canonical Nextcloud URL normalization and
  OneDrive nextLink origin guard
- Full backend suite: 585 passed, 1 pre-existing failure (missing python-docx module)
2026-06-22 08:18:23 +02:00

92 lines
3.7 KiB
Python

"""
NextcloudBackend — Nextcloud-specific WebDAV StorageBackend implementation.
Extends WebDAVBackend with Nextcloud path conventions.
Inheritance:
NextcloudBackend → WebDAVBackend → StorageBackend
All 7 StorageBackend abstract methods are inherited from WebDAVBackend.
list_folder is inherited from WebDAVBackend — NextcloudBackend does NOT
override it so that both providers share exactly one canonical adapter
implementation (Phase 12.1 P0 fix).
Only health_check is overridden to use the Nextcloud root (empty string
is a valid check target for Nextcloud's WebDAV endpoint).
Nextcloud WebDAV path convention:
The server_url must be the normalized Nextcloud WebDAV root for a user:
https://nc.example.com/remote.php/dav/files/{username}/
Use normalize_nextcloud_url(base_url, username) at construction time.
Object keys are relative to this root (inherited from WebDAVBackend).
SSRF prevention:
Inherited from WebDAVBackend.__init__ — validate_cloud_url(server_url) is
called before Client construction and before every outbound request (D-17).
Credentials dict shape (same as WebDAVBackend):
{"server_url": str, "username": str, "password": str}
"""
from __future__ import annotations
import asyncio
from storage.cloud_utils import validate_cloud_url
from storage.webdav_backend import WebDAVBackend
class NextcloudBackend(WebDAVBackend):
"""Nextcloud storage backend — extends WebDAVBackend.
The server_url must be the full normalized Nextcloud WebDAV root:
https://nc.example.com/remote.php/dav/files/{username}/
All 7 StorageBackend methods and list_folder (CloudResourceAdapter) are
inherited from WebDAVBackend — no public override. Nextcloud-specific
URL normalization is handled by normalize_nextcloud_url() in cloud_utils
before construction.
The SSRF guard is fully inherited — any private/localhost URL raises
ValueError at construction time (and before every outbound request).
"""
def __init__(self, server_url: str, username: str, password: str) -> None:
"""Construct a NextcloudBackend.
Args:
server_url: Nextcloud WebDAV root URL.
Should be pre-normalized via normalize_nextcloud_url().
E.g. "https://nc.example.com/remote.php/dav/files/alice/"
username: Nextcloud username (stored for path convention context).
password: Nextcloud account password or app-specific password (D-07).
Raises:
ValueError: If server_url targets a private/internal address (inherited SSRF guard).
"""
super().__init__(server_url, username, password)
self._username = username
# list_folder is intentionally NOT overridden.
# NextcloudBackend inherits WebDAVBackend.list_folder which implements the
# canonical (connection_id, user_id, parent_ref=None, page_token=None) -> CloudListing
# contract. A legacy list_folder(folder_path="") -> list[dict] override was
# the root cause of the Phase 12.1 P0 defect (TypeError on canonical invocation).
async def health_check(self) -> bool:
"""Return True if the Nextcloud WebDAV endpoint is reachable.
Uses client.check("") to probe the WebDAV root path rather than "/"
because some Nextcloud configurations expect the base path check on
the root without a trailing slash component.
Returns:
True if the server is reachable, False otherwise.
"""
try:
# Re-validate before every outbound request (D-17)
validate_cloud_url(self._server_url)
result = await asyncio.to_thread(self._client.check, "")
return bool(result)
except Exception:
return False