Refactor storage to provider-agnostic band-scoped model
Replaces per-member Nextcloud credentials with a BandStorage model that supports multiple providers. Credentials are Fernet-encrypted at rest; worker receives audio via an internal streaming endpoint instead of direct storage access. - Add BandStorage DB model with partial unique index (one active per band) - Add migrations 0007 (create band_storage) and 0008 (drop old nc columns) - Add StorageFactory that builds the correct StorageClient from BandStorage - Add storage router: connect/nextcloud, OAuth2 authorize/callback, list, disconnect - Add Fernet encryption helpers in security/encryption.py - Rewrite watcher for per-band polling via internal API config endpoint - Update worker to stream audio from API instead of accessing storage directly - Update frontend: new storage API in bands.ts, rewritten StorageSection, simplified band creation modal (no storage step) - Add STORAGE_ENCRYPTION_KEY to all docker-compose files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -25,19 +26,11 @@ class NextcloudClient:
|
||||
if not base_url or not username:
|
||||
raise ValueError("Nextcloud credentials must be provided explicitly")
|
||||
self._base = base_url.rstrip("/")
|
||||
self._username = username
|
||||
self._auth = (username, password)
|
||||
self._dav_root = f"{self._base}/remote.php/dav/files/{self._auth[0]}"
|
||||
|
||||
@classmethod
|
||||
def for_member(cls, member: object) -> NextcloudClient | None:
|
||||
"""Return a client using member's personal NC credentials if configured.
|
||||
Returns None if member has no Nextcloud configuration."""
|
||||
nc_url = getattr(member, "nc_url", None)
|
||||
nc_username = getattr(member, "nc_username", None)
|
||||
nc_password = getattr(member, "nc_password", None)
|
||||
if nc_url and nc_username and nc_password:
|
||||
return cls(base_url=nc_url, username=nc_username, password=nc_password)
|
||||
return None
|
||||
self._dav_root = f"{self._base}/remote.php/dav/files/{username}"
|
||||
# Prefix stripped from WebDAV hrefs to produce relative paths
|
||||
self._dav_prefix = f"/remote.php/dav/files/{username}/"
|
||||
|
||||
def _client(self) -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(auth=self._auth, timeout=30.0)
|
||||
@@ -83,7 +76,17 @@ class NextcloudClient:
|
||||
content=body,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return _parse_propfind_multi(resp.text)
|
||||
items = _parse_propfind_multi(resp.text)
|
||||
# Normalise WebDAV absolute hrefs to provider-relative paths so callers
|
||||
# never need to know about DAV internals. URL-decode to handle
|
||||
# filenames that contain spaces or non-ASCII characters.
|
||||
for item in items:
|
||||
decoded = unquote(item.path)
|
||||
if decoded.startswith(self._dav_prefix):
|
||||
item.path = decoded[len(self._dav_prefix):]
|
||||
else:
|
||||
item.path = decoded.lstrip("/")
|
||||
return items
|
||||
|
||||
async def download(self, path: str) -> bytes:
|
||||
logger.debug("Downloading file from Nextcloud: %s", path)
|
||||
|
||||
Reference in New Issue
Block a user