SurfSense/surfsense_backend/app/routes/export_routes.py
CREDO23 56826a63bc refactor(routes): rename to workspace + consolidate URL spellings (Phase 2 Wave E)
Hard cutover of the HTTP surface: collapse the three legacy spellings
(/searchspaces, /search-spaces, /search-space/{id}) onto canonical
/workspaces/{workspace_id}/..., rename the gateway field-setter sub-actions to
singular /workspace, rename search_spaces_routes.py -> workspaces_routes.py and the
search_spaces_router -> workspaces_router include, and flip search_space_id ->
workspace_id in bodies/params. No alias routers: the old URLs now 404 by design.
Full app constructs with zero legacy path spellings.
2026-06-26 18:35:08 +02:00

64 lines
1.9 KiB
Python

"""Routes for exporting knowledge base content as ZIP."""
import logging
import os
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.context import AuthContext
from app.db import Permission, get_async_session
from app.services.export_service import build_export_zip
from app.users import get_auth_context
from app.utils.rbac import check_permission
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/workspaces/{workspace_id}/export")
async def export_knowledge_base(
workspace_id: int,
folder_id: int | None = Query(
None, description="Export only this folder's subtree"
),
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
):
"""Export documents as a ZIP of markdown files preserving folder structure."""
await check_permission(
session,
auth,
workspace_id,
Permission.DOCUMENTS_READ.value,
"You don't have permission to export documents in this workspace",
)
try:
result = await build_export_zip(session, workspace_id, folder_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) from None
def stream_and_cleanup():
try:
with open(result.zip_path, "rb") as f:
while chunk := f.read(8192):
yield chunk
finally:
os.unlink(result.zip_path)
headers = {
"Content-Disposition": f'attachment; filename="{result.export_name}.zip"',
"Content-Length": str(result.zip_size),
}
if result.skipped_docs:
headers["X-Skipped-Documents"] = str(len(result.skipped_docs))
return StreamingResponse(
stream_and_cleanup(),
media_type="application/zip",
headers=headers,
)