Skip to content

Commit 6054b7a

Browse files
committed
feat: add wiki ZIP export API endpoint
GET /api/repos/{repo_id}/export returns a ZIP archive with pages organized as wiki/{page_type}/{target_path}.md. Includes tests for success and 404 cases.
1 parent c817bc0 commit 6054b7a

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

packages/server/src/repowise/server/routers/repos.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import io
67
import logging
8+
import zipfile
9+
from pathlib import PurePosixPath
710

811
from sqlalchemy import func, select
912
from sqlalchemy.ext.asyncio import AsyncSession
1013

1114
from fastapi import APIRouter, Depends, HTTPException, Request
15+
from fastapi.responses import StreamingResponse
1216
from repowise.core.persistence import crud
1317
from repowise.core.persistence.models import (
1418
DeadCodeFinding,
@@ -240,3 +244,42 @@ def _on_done(t: asyncio.Task) -> None:
240244
logger.error("background_job_failed", exc_info=t.exception())
241245

242246
task.add_done_callback(_on_done)
247+
248+
249+
@router.get("/{repo_id}/export")
250+
async def export_wiki(
251+
repo_id: str,
252+
session: AsyncSession = Depends(get_db_session), # noqa: B008
253+
) -> StreamingResponse:
254+
"""Export all wiki pages as a ZIP of markdown files with folder structure."""
255+
repo = await crud.get_repository(session, repo_id)
256+
if repo is None:
257+
raise HTTPException(status_code=404, detail="Repository not found")
258+
259+
pages = await crud.list_pages(session, repo_id, limit=10000)
260+
if not pages:
261+
raise HTTPException(status_code=404, detail="No pages to export")
262+
263+
buf = io.BytesIO()
264+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
265+
for page in pages:
266+
target = page.target_path or page.id
267+
safe = (
268+
target.replace("::", "/")
269+
.replace("->", "--")
270+
.replace("\\", "/")
271+
)
272+
path = PurePosixPath("wiki") / page.page_type / safe
273+
if path.suffix != ".md":
274+
path = path.with_suffix(path.suffix + ".md")
275+
276+
content = f"# {page.title}\n\n{page.content}"
277+
zf.writestr(str(path), content)
278+
279+
buf.seek(0)
280+
filename = f"{repo.name}-wiki.zip"
281+
return StreamingResponse(
282+
buf,
283+
media_type="application/zip",
284+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
285+
)

tests/unit/server/test_repos.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,33 @@ async def test_full_resync_duplicate_returns_409(client: AsyncClient) -> None:
125125

126126
resp2 = await client.post(f"/api/repos/{repo['id']}/full-resync")
127127
assert resp2.status_code == 409
128+
129+
130+
131+
@pytest.mark.asyncio
132+
async def test_export_wiki_not_found(client: AsyncClient) -> None:
133+
resp = await client.get("/api/repos/nonexistent/export")
134+
assert resp.status_code == 404
135+
136+
137+
@pytest.mark.asyncio
138+
async def test_export_wiki_returns_zip(client: AsyncClient, session) -> None:
139+
import zipfile
140+
from io import BytesIO
141+
142+
from repowise.core.persistence.crud import upsert_page, upsert_repository
143+
from tests.unit.persistence.helpers import make_page_kwargs
144+
145+
repo = await upsert_repository(session, name="export-test", local_path="/tmp/export-test")
146+
await upsert_page(session, **make_page_kwargs(repo.id))
147+
await session.commit()
148+
149+
resp = await client.get(f"/api/repos/{repo.id}/export")
150+
assert resp.status_code == 200
151+
assert resp.headers["content-type"] == "application/zip"
152+
153+
zf = zipfile.ZipFile(BytesIO(resp.content))
154+
names = zf.namelist()
155+
assert len(names) == 1
156+
assert names[0].startswith("wiki/")
157+
assert names[0].endswith(".md")

0 commit comments

Comments
 (0)