fix(05): cloud API path param, root sentinel, webdav creds in list, upload path

cloud.py: list_connections now decrypts and surfaces server_url +
connection_username for nextcloud/webdav providers; folder route uses
{folder_id:path} to handle slashes; translates "root" sentinel to "".
nextcloud_backend.py: skip parent directory entry in PROPFIND Depth:1 results.
webdav_backend.py: add cloud_folder + original_filename params to
upload_object so files land in the user's chosen folder with their real name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-30 11:58:01 +02:00
parent 67edc19a36
commit 54ef3357ba
3 changed files with 37 additions and 24 deletions
+15 -20
View File
@@ -30,6 +30,7 @@ from __future__ import annotations
import asyncio
import io
import urllib.parse
from pathlib import Path
from webdav3.client import Client
@@ -96,31 +97,25 @@ class WebDAVBackend(StorageBackend):
file_bytes: bytes,
extension: str,
content_type: str,
cloud_folder: str | None = None,
original_filename: str | None = None,
) -> str:
"""Upload bytes to WebDAV and return the object_key (WebDAV path).
Ensures the parent directory "docuvault/{user_id}/" exists before upload
by calling client.mkdir() with recursive=True. webdavclient3 mkdir is a
no-op if the directory already exists.
Args:
user_id: User UUID string.
document_id: Document UUID string.
file_bytes: Raw file content.
extension: File extension with leading dot (e.g. ".pdf").
content_type: MIME type (unused by WebDAV, kept for ABC compliance).
Returns:
object_key: The WebDAV path where the file was stored.
Raises:
ValueError: If SSRF guard fires on re-validation (D-17).
When cloud_folder is provided the file is stored inside that folder
(e.g. "Documents/") using the original filename so it appears naturally
in the user's cloud folder browser. When omitted the default
DocuVault-managed UUID path is used.
"""
# Re-validate before every outbound request (D-17 / T-05-04-02)
validate_cloud_url(self._server_url)
object_key = self._make_path(user_id, document_id, extension)
# Ensure parent directory exists (idempotent)
parent_dir = f"docuvault/{urllib.parse.quote(str(user_id), safe='')}"
if cloud_folder:
parent_dir = cloud_folder.rstrip("/")
# Use original filename (basename only — path traversal guard)
safe_name = Path(original_filename).name if original_filename else f"{document_id}{extension}"
object_key = f"{parent_dir}/{safe_name}" if parent_dir else safe_name
else:
object_key = self._make_path(user_id, document_id, extension)
parent_dir = f"docuvault/{urllib.parse.quote(str(user_id), safe='')}"
await asyncio.to_thread(self._client.mkdir, parent_dir, True)
buf = io.BytesIO(file_bytes)
await asyncio.to_thread(self._client.upload_to, buf, object_key)