from __future__ import annotations from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from govoplan_core.auth import ApiPrincipal, require_scope from govoplan_core.audit.logging import audit_from_principal from govoplan_core.db.session import get_session from govoplan_files.backend.route_support import ( _asset_response, _connector_space_response, _http_error, _is_admin, ) from govoplan_files.backend.schemas import ( FileAssetResponse, FileBlobGcRequest, FileBlobGcResponse, FileConnectorSpaceResponse, FileFolderDeleteRequest, FileFolderRestoreResponse, FileLifecycleUpdateRequest, FilePurgeExecuteRequest, FilePurgePreviewItem, FilePurgePreviewRequest, FilePurgePreviewResponse, FilePurgeResponse, FileRestoreResponse, ) from govoplan_files.backend.storage.common import FileStorageError from govoplan_files.backend.storage.lifecycle import ( execute_asset_purge, garbage_collect_unreferenced_blobs, get_asset_for_lifecycle, preview_asset_purge, restore_asset, restore_connector_space, restore_folder, set_asset_lifecycle, ) router = APIRouter(prefix="/files", tags=["files-lifecycle"]) @router.patch("/{file_id}/lifecycle", response_model=FileAssetResponse) def update_file_lifecycle( file_id: str, payload: FileLifecycleUpdateRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:retention")), ): try: asset = get_asset_for_lifecycle( session, tenant_id=principal.tenant_id, user_id=principal.user.id, asset_id=file_id, is_admin=_is_admin(principal), ) set_asset_lifecycle( session, asset, retained_until=payload.retained_until, legal_hold=payload.legal_hold, reason=payload.reason, expected_revision=payload.expected_revision, ) audit_from_principal( session, principal, action="files.lifecycle.updated", object_type="file", object_id=asset.id, details={ "retained_until": ( asset.retained_until.isoformat() if asset.retained_until else None ), "legal_hold": asset.legal_hold, "lifecycle_revision": asset.lifecycle_revision, "reason": asset.lifecycle_reason, }, ) session.commit() return _asset_response(session, asset, include_shares=True) except FileStorageError as exc: session.rollback() raise _http_error(exc, not_found=True) from exc @router.post("/assets/{file_id}/restore", response_model=FileRestoreResponse) def restore_file( file_id: str, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:restore")), ): try: asset = get_asset_for_lifecycle( session, tenant_id=principal.tenant_id, user_id=principal.user.id, asset_id=file_id, is_admin=_is_admin(principal), ) changed = restore_asset(session, asset) if changed: audit_from_principal( session, principal, action="files.file.restored", object_type="file", object_id=asset.id, details={"display_path": asset.display_path}, ) session.commit() return FileRestoreResponse(restored_count=1 if changed else 0) except FileStorageError as exc: session.rollback() raise _http_error(exc, not_found=True) from exc @router.post("/folders/restore", response_model=FileFolderRestoreResponse) def restore_file_folder( payload: FileFolderDeleteRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:restore")), ): try: restored_folders, restored_files = restore_folder( session, tenant_id=principal.tenant_id, owner_type=payload.owner_type, owner_id=payload.owner_id, user_id=principal.user.id, path=payload.path, recursive=payload.recursive, is_admin=_is_admin(principal), ) audit_from_principal( session, principal, action="files.folder.restored", object_type="file_folder", object_id=payload.path, details={ "restored_folders": restored_folders, "restored_files": restored_files, }, ) session.commit() return FileFolderRestoreResponse( restored_folders=restored_folders, restored_files=restored_files ) except FileStorageError as exc: session.rollback() raise _http_error(exc, not_found=True) from exc @router.post( "/connector-spaces/{space_id}/restore", response_model=FileConnectorSpaceResponse, ) def restore_file_connector_space( space_id: str, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:restore")), ): try: space = restore_connector_space( session, tenant_id=principal.tenant_id, user_id=principal.user.id, space_id=space_id, is_admin=_is_admin(principal), ) audit_from_principal( session, principal, action="files.connector_space.restored", object_type="file_connector_space", object_id=space.id, details={"label": space.label}, ) session.commit() return _connector_space_response(space) except FileStorageError as exc: session.rollback() raise _http_error(exc, not_found=True) from exc @router.post("/purge/preview", response_model=FilePurgePreviewResponse) def preview_file_purge( payload: FilePurgePreviewRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:purge")), ): try: preview = preview_asset_purge( session, tenant_id=principal.tenant_id, file_ids=payload.file_ids ) return FilePurgePreviewResponse( preview_sha256=preview.preview_sha256, eligible=preview.eligible, items=[ FilePurgePreviewItem( file_id=item.file_id, filename=item.filename, lifecycle_revision=item.lifecycle_revision, deleted_at=(item.deleted_at.isoformat() if item.deleted_at else None), retained_until=( item.retained_until.isoformat() if item.retained_until else None ), legal_hold=item.legal_hold, blockers=list(item.blockers), blob_ids=list(item.blob_ids), ) for item in preview.items ], ) except FileStorageError as exc: raise _http_error(exc) from exc @router.post("/purge/execute", response_model=FilePurgeResponse) def execute_file_purge( payload: FilePurgeExecuteRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:purge")), ): try: result = execute_asset_purge( session, tenant_id=principal.tenant_id, file_ids=payload.file_ids, preview_sha256=payload.preview_sha256, idempotency_key=payload.idempotency_key, approval_reference=payload.approval_reference, before_commit=lambda assets: audit_from_principal( session, principal, action="files.files.purged", object_type="file_batch", object_id=payload.preview_sha256, details={ "file_ids": [asset.id for asset in assets], "approval_reference": payload.approval_reference, "preview_sha256": payload.preview_sha256, }, ), ) return FilePurgeResponse( recovery_operation_id=result.recovery_operation_id, status=result.status, replayed=result.replayed, purged_files=result.purged_files, released_blobs=result.released_blobs, ) except FileStorageError as exc: session.rollback() raise _http_error(exc) from exc @router.post("/purge/blobs", response_model=FileBlobGcResponse) def garbage_collect_file_blobs( payload: FileBlobGcRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("files:file:purge")), ): try: result = garbage_collect_unreferenced_blobs( session, tenant_id=principal.tenant_id, limit=payload.limit, approval_reference=payload.approval_reference, ) audit_from_principal( session, principal, action="files.blobs.garbage_collected", object_type="file_blob_batch", object_id=principal.tenant_id, details={ "inspected_blobs": result.inspected_blobs, "deleted_blobs": result.deleted_blobs, "unresolved_operation_ids": list(result.unresolved_operation_ids), "approval_reference": payload.approval_reference, }, commit=True, ) return FileBlobGcResponse( inspected_blobs=result.inspected_blobs, deleted_blobs=result.deleted_blobs, unresolved_operation_ids=list(result.unresolved_operation_ids), ) except FileStorageError as exc: session.rollback() raise _http_error(exc) from exc