- Remove incompatible NextcloudBackend.list_folder(folder_path="") override so Nextcloud inherits canonical WebDAVBackend.list_folder(connection_id, user_id, parent_ref=None, page_token=None) -> CloudListing - Add normalize_nextcloud_url() to cloud_utils.py: idempotent canonical DAV root derivation, username percent-encoding, https-only, rejects userinfo/ query/fragment, validates via SSRF guard before return - Use normalize_nextcloud_url in cloud_backend_factory for nextcloud provider - Add TestNextcloudBackendNoListFolderOverride and TestNextcloudUrlNormalization to test_webdav_backend.py - All 9 Nextcloud contract failures now pass; full focused suite: 158 passed
90 lines
3.7 KiB
Python
90 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
|
|
|
|
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
|