Files
kite/backend/storage/cloud_backend_factory.py
T
curo1305 88a62da4c4 feat(13-03): extend cloud contract with mutable adapter and provider implementations
- Add MutableCloudResourceAdapter abstract class to cloud_base.py with Phase 13
  mutable-operation vocabulary (MUT_KIND_* / MUT_REASON_* constants, PreviewSupport)
- Add abstract methods: create_folder, rename, move, delete, upload_file + _normalize_error
- Update GoogleDriveBackend to implement MutableCloudResourceAdapter with Drive-scope
  methods; prefer trash() for delete (D-11); update SCOPES to drive (D-17)
- Update OneDriveBackend to implement MutableCloudResourceAdapter; use
  PublicClientApplication for refresh_token flow; Graph DELETE is always permanent (D-11)
- Update WebDAVBackend to implement MutableCloudResourceAdapter; SSRF guard via __init__
  (not per-call) for mutable methods; _validate_destination for MOVE SSRF (T-13-04)
- NextcloudBackend inherits all mutable methods from WebDAVBackend (no changes needed)
- Add build_mutable_cloud_adapter() to cloud_backend_factory.py
- All 184 tests pass (test_cloud_backends.py + test_cloud_provider_contract.py)
2026-06-22 18:31:53 +02:00

87 lines
3.3 KiB
Python

"""Factory for user-scoped cloud storage backends."""
from __future__ import annotations
from storage.cloud_base import CloudResourceAdapter, MutableCloudResourceAdapter
def build_cloud_backend(provider: str, credentials: dict):
if provider == "google_drive":
from storage.google_drive_backend import GoogleDriveBackend # lazy import
return GoogleDriveBackend(credentials)
if provider == "onedrive":
from storage.onedrive_backend import OneDriveBackend # lazy import
return OneDriveBackend(credentials)
if provider == "nextcloud":
from storage.nextcloud_backend import NextcloudBackend # lazy import
from storage.cloud_utils import normalize_nextcloud_url # lazy import
server_url = credentials["server_url"]
username = credentials["username"]
# Normalize to canonical WebDAV root idempotently; validate SSRF before construction
try:
server_url = normalize_nextcloud_url(server_url, username)
except ValueError:
# URL may already be canonical or validation rejected it — let the
# constructor's validate_cloud_url call surface the error
pass
return NextcloudBackend(
server_url,
username,
credentials["password"],
)
if provider == "webdav":
from storage.webdav_backend import WebDAVBackend # lazy import
return WebDAVBackend(
credentials["server_url"],
credentials["username"],
credentials["password"],
)
raise ValueError(f"Unknown provider: {provider}")
def build_cloud_resource_adapter(provider: str, credentials: dict) -> CloudResourceAdapter:
"""Build a CloudResourceAdapter for the given provider and credentials.
Returns a provider instance that implements the CloudResourceAdapter read-only
interface (list_folder, get_capabilities, merge_item_capabilities). All four
supported providers implement CloudResourceAdapter as a mixin alongside
their StorageBackend interface.
Raises:
ValueError: If provider is not one of the four supported cloud providers.
"""
backend = build_cloud_backend(provider, credentials)
if not isinstance(backend, CloudResourceAdapter):
raise ValueError(
f"Provider {provider!r} backend does not implement CloudResourceAdapter"
)
return backend
def build_mutable_cloud_adapter(provider: str, credentials: dict) -> MutableCloudResourceAdapter:
"""Build a MutableCloudResourceAdapter for the given provider and credentials.
Returns a provider instance that implements the Phase 13 mutable-operation
contract (create_folder, rename, move, delete, upload_file) in addition to
the read-only CloudResourceAdapter interface.
Raises:
ValueError: If provider is not one of the four supported cloud providers,
or if the provider backend does not implement MutableCloudResourceAdapter.
"""
backend = build_cloud_backend(provider, credentials)
if not isinstance(backend, MutableCloudResourceAdapter):
raise ValueError(
f"Provider {provider!r} backend does not implement MutableCloudResourceAdapter. "
"All Phase 13 providers must subclass MutableCloudResourceAdapter."
)
return backend