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:
@@ -7,7 +7,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from rehearsalhub.db.models import Band, BandInvite, BandMember
|
||||
from rehearsalhub.db.models import Band, BandInvite, BandMember, BandStorage
|
||||
from rehearsalhub.repositories.base import BaseRepository
|
||||
|
||||
|
||||
@@ -92,16 +92,27 @@ class BandRepository(BaseRepository[Band]):
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_nc_folder_prefix(self, path: str) -> Band | None:
|
||||
"""Return the band whose nc_folder_path is a prefix of path."""
|
||||
stmt = select(Band).where(Band.nc_folder_path.is_not(None))
|
||||
"""Return the band whose active storage root_path is a prefix of *path*.
|
||||
|
||||
Longest match wins (most-specific prefix) so nested paths resolve correctly.
|
||||
"""
|
||||
stmt = (
|
||||
select(Band, BandStorage.root_path)
|
||||
.join(
|
||||
BandStorage,
|
||||
(BandStorage.band_id == Band.id) & BandStorage.is_active.is_(True),
|
||||
)
|
||||
.where(BandStorage.root_path.is_not(None))
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
bands = result.scalars().all()
|
||||
# Longest match wins (most specific prefix)
|
||||
rows = result.all()
|
||||
best: Band | None = None
|
||||
for band in bands:
|
||||
folder = band.nc_folder_path # type: ignore[union-attr]
|
||||
if path.startswith(folder) and (best is None or len(folder) > len(best.nc_folder_path)): # type: ignore[arg-type]
|
||||
best_len = 0
|
||||
for band, root_path in rows:
|
||||
folder = root_path.rstrip("/") + "/"
|
||||
if path.startswith(folder) and len(folder) > best_len:
|
||||
best = band
|
||||
best_len = len(folder)
|
||||
return best
|
||||
|
||||
async def list_for_member(self, member_id: uuid.UUID) -> list[Band]:
|
||||
|
||||
Reference in New Issue
Block a user