249 lines
7.9 KiB
Python
249 lines
7.9 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
from sqlalchemy import false, or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_files.backend.db.models import FileConnectorSpace
|
|
from govoplan_files.backend.storage.access import ensure_owner_access, user_group_ids
|
|
from govoplan_files.backend.storage.common import FileStorageError, utcnow
|
|
from govoplan_files.backend.storage.connector_browse import normalize_connector_browse_path
|
|
from govoplan_files.backend.storage.connector_profiles import ConnectorProfile
|
|
|
|
|
|
SYNC_MODES = {"manual"}
|
|
|
|
|
|
def connector_space_owner_id(space: FileConnectorSpace) -> str:
|
|
return space.owner_user_id if space.owner_type == "user" else space.owner_group_id # type: ignore[return-value]
|
|
|
|
|
|
def create_connector_space(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
owner_type: str,
|
|
owner_id: str,
|
|
user_id: str,
|
|
label: str,
|
|
profile: ConnectorProfile,
|
|
library_id: str | None = None,
|
|
remote_path: str | None = None,
|
|
sync_mode: str = "manual",
|
|
read_only: bool = True,
|
|
metadata: Mapping[str, Any] | None = None,
|
|
is_admin: bool = False,
|
|
) -> FileConnectorSpace:
|
|
owner_type = owner_type.lower().strip()
|
|
ensure_owner_access(session, tenant_id=tenant_id, owner_type=owner_type, owner_id=owner_id, user_id=user_id, is_admin=is_admin)
|
|
label = _normalize_label(label)
|
|
sync_mode = _normalize_sync_mode(sync_mode)
|
|
remote_path = normalize_connector_browse_path(remote_path)
|
|
library_id = _clean_optional(library_id)
|
|
|
|
existing = _owned_query(session, tenant_id=tenant_id, owner_type=owner_type, owner_id=owner_id).filter(
|
|
FileConnectorSpace.label == label,
|
|
FileConnectorSpace.deleted_at.is_(None),
|
|
).first()
|
|
if existing is not None:
|
|
raise FileStorageError(f"Connector space already exists: {label}")
|
|
|
|
space = FileConnectorSpace(
|
|
tenant_id=tenant_id,
|
|
owner_type=owner_type,
|
|
owner_user_id=owner_id if owner_type == "user" else None,
|
|
owner_group_id=owner_id if owner_type == "group" else None,
|
|
label=label,
|
|
connector_profile_id=profile.id,
|
|
provider=profile.provider,
|
|
library_id=library_id,
|
|
remote_path=remote_path,
|
|
sync_mode=sync_mode,
|
|
read_only=read_only,
|
|
is_active=True,
|
|
created_by_user_id=user_id,
|
|
metadata_=dict(metadata or {}),
|
|
)
|
|
session.add(space)
|
|
session.flush()
|
|
return space
|
|
|
|
|
|
def list_connector_spaces_for_user(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str,
|
|
owner_type: str | None = None,
|
|
owner_id: str | None = None,
|
|
include_inactive: bool = False,
|
|
is_admin: bool = False,
|
|
) -> list[FileConnectorSpace]:
|
|
query = session.query(FileConnectorSpace).filter(
|
|
FileConnectorSpace.tenant_id == tenant_id,
|
|
FileConnectorSpace.deleted_at.is_(None),
|
|
)
|
|
if not include_inactive:
|
|
query = query.filter(FileConnectorSpace.is_active.is_(True))
|
|
|
|
if owner_type:
|
|
if not owner_id:
|
|
raise FileStorageError("owner_id is required when owner_type is set")
|
|
owner_type = owner_type.lower().strip()
|
|
ensure_owner_access(session, tenant_id=tenant_id, owner_type=owner_type, owner_id=owner_id, user_id=user_id, is_admin=is_admin)
|
|
query = _owner_filter(query, owner_type, owner_id)
|
|
elif not is_admin:
|
|
group_ids = user_group_ids(session, tenant_id=tenant_id, user_id=user_id)
|
|
query = query.filter(
|
|
or_(
|
|
FileConnectorSpace.owner_user_id == user_id,
|
|
FileConnectorSpace.owner_group_id.in_(group_ids) if group_ids else false(),
|
|
)
|
|
)
|
|
|
|
return query.order_by(FileConnectorSpace.owner_type.asc(), FileConnectorSpace.label.asc()).all()
|
|
|
|
|
|
def get_connector_space_for_user(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str,
|
|
space_id: str,
|
|
include_inactive: bool = False,
|
|
is_admin: bool = False,
|
|
) -> FileConnectorSpace:
|
|
query = session.query(FileConnectorSpace).filter(
|
|
FileConnectorSpace.id == space_id,
|
|
FileConnectorSpace.tenant_id == tenant_id,
|
|
FileConnectorSpace.deleted_at.is_(None),
|
|
)
|
|
if not include_inactive:
|
|
query = query.filter(FileConnectorSpace.is_active.is_(True))
|
|
space = query.one_or_none()
|
|
if space is None:
|
|
raise FileStorageError("Connector space not found")
|
|
ensure_owner_access(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
owner_type=space.owner_type,
|
|
owner_id=connector_space_owner_id(space),
|
|
user_id=user_id,
|
|
is_admin=is_admin,
|
|
)
|
|
return space
|
|
|
|
|
|
def update_connector_space(
|
|
session: Session,
|
|
space: FileConnectorSpace,
|
|
*,
|
|
user_id: str,
|
|
label: str | None = None,
|
|
library_id: str | None = None,
|
|
remote_path: str | None = None,
|
|
sync_mode: str | None = None,
|
|
is_active: bool | None = None,
|
|
metadata: Mapping[str, Any] | None = None,
|
|
is_admin: bool = False,
|
|
) -> FileConnectorSpace:
|
|
ensure_owner_access(
|
|
session,
|
|
tenant_id=space.tenant_id,
|
|
owner_type=space.owner_type,
|
|
owner_id=connector_space_owner_id(space),
|
|
user_id=user_id,
|
|
is_admin=is_admin,
|
|
)
|
|
if label is not None:
|
|
new_label = _normalize_label(label)
|
|
existing = _owned_query(
|
|
session,
|
|
tenant_id=space.tenant_id,
|
|
owner_type=space.owner_type,
|
|
owner_id=connector_space_owner_id(space),
|
|
).filter(
|
|
FileConnectorSpace.id != space.id,
|
|
FileConnectorSpace.label == new_label,
|
|
FileConnectorSpace.deleted_at.is_(None),
|
|
).first()
|
|
if existing is not None:
|
|
raise FileStorageError(f"Connector space already exists: {new_label}")
|
|
space.label = new_label
|
|
if library_id is not None:
|
|
space.library_id = _clean_optional(library_id)
|
|
if remote_path is not None:
|
|
space.remote_path = normalize_connector_browse_path(remote_path)
|
|
if sync_mode is not None:
|
|
space.sync_mode = _normalize_sync_mode(sync_mode)
|
|
if is_active is not None:
|
|
space.is_active = bool(is_active)
|
|
if metadata is not None:
|
|
space.metadata_ = dict(metadata)
|
|
session.add(space)
|
|
session.flush()
|
|
return space
|
|
|
|
|
|
def soft_delete_connector_space(
|
|
session: Session,
|
|
space: FileConnectorSpace,
|
|
*,
|
|
user_id: str,
|
|
is_admin: bool = False,
|
|
) -> FileConnectorSpace:
|
|
ensure_owner_access(
|
|
session,
|
|
tenant_id=space.tenant_id,
|
|
owner_type=space.owner_type,
|
|
owner_id=connector_space_owner_id(space),
|
|
user_id=user_id,
|
|
is_admin=is_admin,
|
|
)
|
|
space.deleted_at = utcnow()
|
|
space.is_active = False
|
|
session.add(space)
|
|
session.flush()
|
|
return space
|
|
|
|
|
|
def _owned_query(session: Session, *, tenant_id: str, owner_type: str, owner_id: str):
|
|
query = session.query(FileConnectorSpace).filter(
|
|
FileConnectorSpace.tenant_id == tenant_id,
|
|
FileConnectorSpace.owner_type == owner_type,
|
|
)
|
|
return _owner_filter(query, owner_type, owner_id)
|
|
|
|
|
|
def _owner_filter(query, owner_type: str, owner_id: str):
|
|
if owner_type == "user":
|
|
return query.filter(FileConnectorSpace.owner_user_id == owner_id)
|
|
if owner_type == "group":
|
|
return query.filter(FileConnectorSpace.owner_group_id == owner_id)
|
|
raise FileStorageError("Files must be owned by a user or group")
|
|
|
|
|
|
def _normalize_label(value: str) -> str:
|
|
label = value.strip()
|
|
if not label:
|
|
raise FileStorageError("Connector space label is required")
|
|
if len(label) > 255:
|
|
raise FileStorageError("Connector space label is too long")
|
|
return label
|
|
|
|
|
|
def _normalize_sync_mode(value: str) -> str:
|
|
mode = value.strip().casefold()
|
|
if mode not in SYNC_MODES:
|
|
raise FileStorageError(f"Unsupported connector space sync mode: {value}")
|
|
return mode
|
|
|
|
|
|
def _clean_optional(value: object) -> str | None:
|
|
if value is None:
|
|
return None
|
|
text = str(value).strip()
|
|
return text or None
|