Source code for app.routes_save
"""
Character-description save and export routes.
This router owns the default-system-prompt, save, and save-export endpoints.
It keeps HTTP concerns in the route layer while delegating save-folder
creation and file writing to the service layer.
"""
from __future__ import annotations
import io
import re
from fastapi import APIRouter, HTTPException
from fastapi.responses import PlainTextResponse, StreamingResponse
from app.config import DATA_DIR as _DATA_DIR
from app.file_loaders import load_default_prompt
from app.save_package import create_zip_archive
from app.schema import SaveRequest, SaveResponse
from app.services.save_service import save_run as persist_save_run
router = APIRouter(tags=["save"])
_FOLDER_NAME_PATTERN = re.compile(r"^\d{8}_\d{6}_[0-9a-f]{8}$")
[docs]
@router.get(
"/api/system-prompt",
response_class=PlainTextResponse,
summary="Return the default system prompt text",
)
def get_system_prompt() -> str:
"""
Return the default Character Description prompt as plain text.
This allows the frontend to resolve the effective system prompt when no
custom override is set, ensuring saved files always contain the complete
prompt text rather than a placeholder.
"""
return load_default_prompt()
[docs]
@router.post(
"/api/save",
response_model=SaveResponse,
summary="Save current session state under the configured writable save root",
)
def save_run(req: SaveRequest) -> SaveResponse:
"""Persist a character-description session under the configured save root."""
return persist_save_run(req, _DATA_DIR)
[docs]
@router.get(
"/api/save/{folder_name}/export",
summary="Download a save package as a zip file",
)
def export_save(folder_name: str) -> StreamingResponse:
"""
Stream a save folder as a compressed zip archive for download.
The endpoint validates the folder name against a strict pattern to
prevent path-traversal attacks, then bundles all known save-package
files into a zip using ``save_package.create_zip_archive()``.
"""
if not _FOLDER_NAME_PATTERN.match(folder_name):
raise HTTPException(
status_code=400,
detail=f"Invalid folder name format: '{folder_name}'.",
)
save_dir = _DATA_DIR / folder_name
if not save_dir.is_dir():
raise HTTPException(
status_code=404,
detail=f"Save folder not found: '{folder_name}'.",
)
zip_bytes = create_zip_archive(save_dir)
return StreamingResponse(
io.BytesIO(zip_bytes),
media_type="application/zip",
headers={
"Content-Disposition": f'attachment; filename="{folder_name}.zip"',
},
)