from __future__ import annotations import json from typing import Literal from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from govoplan_core.auth import ApiPrincipal, require_scope from govoplan_files.backend.change_tracking import ( FILES_CONNECTOR_SPACES_COLLECTION, ) from govoplan_files.backend.schemas import ( FileConnectorSpaceCreateRequest, FileConnectorSpaceResponse, FileConnectorSpacesResponse, FileConnectorSpaceUpdateRequest, FileSpaceResponse, FileSpacesResponse, ) from govoplan_core.db.session import get_session from govoplan_files.backend.storage.access import group_refs_for_ids, user_group_ids from govoplan_files.backend.storage.common import FileStorageError from govoplan_files.backend.storage.connector_spaces import ( connector_space_owner_id, create_connector_space, get_connector_space_for_user, list_connector_spaces_for_user, soft_delete_connector_space, update_connector_space, ) from govoplan_files.backend.storage.connector_policy import ( ConnectorPolicyDenied, ) from govoplan_files.backend.route_support import ( FILES_CONNECTOR_SPACE_RESOURCE, _connector_policy_error, _connector_space_file_space_response, _connector_space_policy_decision, _connector_space_response, _http_error, _is_admin, _record_connector_settings_change, _visible_connector_profile, ) router = APIRouter(prefix="/files", tags=["files"]) @router.get("/spaces", response_model=FileSpacesResponse) def list_file_spaces( session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:read")), ): spaces = [ FileSpaceResponse( id=f"user:{principal.user.id}", label="My files", owner_type="user", owner_id=principal.user.id, description="Files owned by your user account.", ) ] group_ids = user_group_ids( session, tenant_id=principal.tenant_id, user_id=principal.user.id, include_admin_groups=_is_admin(principal), ) if group_ids: groups = group_refs_for_ids(tenant_id=principal.tenant_id, group_ids=group_ids) spaces.extend( FileSpaceResponse( id=f"group:{group.id}", label=f"{group.name} files", owner_type="group", owner_id=group.id, description="Files owned by this group.", ) for group in groups ) connector_spaces = list_connector_spaces_for_user( session, tenant_id=principal.tenant_id, user_id=principal.user.id, is_admin=_is_admin(principal), ) spaces.extend( _connector_space_file_space_response(space) for space in connector_spaces ) return FileSpacesResponse(spaces=spaces) @router.get("/connector-spaces", response_model=FileConnectorSpacesResponse) def list_file_connector_spaces( owner_type: Literal["user", "group"] | None = None, owner_id: str | None = None, include_inactive: bool = False, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:read")), ): try: spaces = list_connector_spaces_for_user( session, tenant_id=principal.tenant_id, user_id=principal.user.id, owner_type=owner_type, owner_id=owner_id, include_inactive=include_inactive and _is_admin(principal), is_admin=_is_admin(principal), ) return FileConnectorSpacesResponse( spaces=[_connector_space_response(space) for space in spaces] ) except FileStorageError as exc: raise _http_error(exc) from exc @router.post( "/connector-spaces", response_model=FileConnectorSpaceResponse, status_code=status.HTTP_201_CREATED, ) def create_file_connector_space( payload: FileConnectorSpaceCreateRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:organize")), ): if payload.owner_type == "group" and not payload.owner_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="owner_id is required for group connector spaces", ) target_owner = payload.owner_id or principal.user.id try: profile = _visible_connector_profile( session, principal, payload.connector_profile_id ) decision = _connector_space_policy_decision( profile, library_id=payload.library_id, remote_path=payload.remote_path, operation="link", ) if not decision.allowed: raise ConnectorPolicyDenied(decision) space = create_connector_space( session, tenant_id=principal.tenant_id, owner_type=payload.owner_type, owner_id=target_owner, user_id=principal.user.id, label=payload.label, profile=profile, library_id=payload.library_id, remote_path=payload.remote_path, sync_mode=payload.sync_mode, metadata=payload.metadata, is_admin=_is_admin(principal), ) _record_connector_settings_change( session, collection=FILES_CONNECTOR_SPACES_COLLECTION, resource_type=FILES_CONNECTOR_SPACE_RESOURCE, resource_id=space.id, operation="created", principal=principal, tenant_id=space.tenant_id, payload={ "owner_type": space.owner_type, "owner_id": connector_space_owner_id(space), }, ) session.commit() return _connector_space_response(space) except ConnectorPolicyDenied as exc: session.rollback() raise _connector_policy_error(exc) from exc except (FileStorageError, ValueError, json.JSONDecodeError) as exc: session.rollback() raise _http_error(exc) from exc @router.patch("/connector-spaces/{space_id}", response_model=FileConnectorSpaceResponse) def update_file_connector_space( space_id: str, payload: FileConnectorSpaceUpdateRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:organize")), ): try: space = get_connector_space_for_user( session, tenant_id=principal.tenant_id, user_id=principal.user.id, space_id=space_id, include_inactive=_is_admin(principal), is_admin=_is_admin(principal), ) except FileStorageError as exc: raise _http_error(exc, not_found=True) from exc try: if payload.library_id is not None or payload.remote_path is not None: profile = _visible_connector_profile( session, principal, space.connector_profile_id ) decision = _connector_space_policy_decision( profile, library_id=payload.library_id if payload.library_id is not None else space.library_id, remote_path=payload.remote_path if payload.remote_path is not None else space.remote_path, operation="link", ) if not decision.allowed: raise ConnectorPolicyDenied(decision) update_connector_space( session, space, user_id=principal.user.id, label=payload.label, library_id=payload.library_id, remote_path=payload.remote_path, sync_mode=payload.sync_mode, is_active=payload.is_active, metadata=payload.metadata, is_admin=_is_admin(principal), ) _record_connector_settings_change( session, collection=FILES_CONNECTOR_SPACES_COLLECTION, resource_type=FILES_CONNECTOR_SPACE_RESOURCE, resource_id=space.id, operation="updated", principal=principal, tenant_id=space.tenant_id, payload={ "owner_type": space.owner_type, "owner_id": connector_space_owner_id(space), }, ) session.commit() return _connector_space_response(space) except ConnectorPolicyDenied as exc: session.rollback() raise _connector_policy_error(exc) from exc except (FileStorageError, ValueError, json.JSONDecodeError) as exc: session.rollback() raise _http_error(exc) from exc @router.delete( "/connector-spaces/{space_id}", response_model=FileConnectorSpaceResponse ) def delete_file_connector_space( space_id: str, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:organize")), ): try: space = get_connector_space_for_user( session, tenant_id=principal.tenant_id, user_id=principal.user.id, space_id=space_id, include_inactive=True, is_admin=_is_admin(principal), ) soft_delete_connector_space( session, space, user_id=principal.user.id, is_admin=_is_admin(principal) ) _record_connector_settings_change( session, collection=FILES_CONNECTOR_SPACES_COLLECTION, resource_type=FILES_CONNECTOR_SPACE_RESOURCE, resource_id=space.id, operation="deleted", principal=principal, tenant_id=space.tenant_id, payload={ "owner_type": space.owner_type, "owner_id": connector_space_owner_id(space), }, ) session.commit() return _connector_space_response(space) except FileStorageError as exc: session.rollback() raise _http_error(exc, not_found=True) from exc