mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-06 22:12:12 +02:00
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.
64 lines
1.9 KiB
Python
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,
|
|
)
|