diff --git a/backend/app.py b/backend/app.py index ea8411cb..c7a4029b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -105,28 +105,18 @@ if not os.environ.get("MIOPEN_LOG_LEVEL"): import torch from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from urllib.parse import quote from . import __version__, config, database from .services import tts, transcribe, llm from .database import get_db from .utils.platform_detect import get_backend_type from .utils.progress import get_progress_manager +# Re-exported for backwards compatibility with callers importing it from here. +from .utils.http import safe_content_disposition as safe_content_disposition from .services.task_queue import create_background_task, init_queue from .routes import register_routers -def safe_content_disposition(disposition_type: str, filename: str) -> str: - """Build a Content-Disposition header safe for non-ASCII filenames. - - Uses RFC 5987 ``filename*`` parameter so browsers can decode UTF-8 - filenames while the ``filename`` fallback stays ASCII-only. - """ - ascii_name = "".join(c for c in filename if c.isascii() and (c.isalnum() or c in " -_.")).strip() or "download" - utf8_name = quote(filename, safe="") - return f"{disposition_type}; filename=\"{ascii_name}\"; filename*=UTF-8''{utf8_name}" - - def create_app() -> FastAPI: """Create and configure the FastAPI application.""" from .mcp_server.server import build_mcp_server, compose_lifespan diff --git a/backend/routes/history.py b/backend/routes/history.py index 694d35be..4b7e2222 100644 --- a/backend/routes/history.py +++ b/backend/routes/history.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from .. import config, models from ..services import export_import, history -from ..app import safe_content_disposition +from ..utils.http import safe_content_disposition from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db router = APIRouter() diff --git a/backend/routes/profiles.py b/backend/routes/profiles.py index 68e5f2af..b5658dea 100644 --- a/backend/routes/profiles.py +++ b/backend/routes/profiles.py @@ -12,7 +12,7 @@ from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.orm import Session from .. import config, models -from ..app import safe_content_disposition +from ..utils.http import safe_content_disposition from ..database import VoiceProfile as DBVoiceProfile, get_db from ..services import channels, export_import, personality, profiles from ..services.profiles import _profile_to_response diff --git a/backend/routes/stories.py b/backend/routes/stories.py index 73757d34..0c6237a8 100644 --- a/backend/routes/stories.py +++ b/backend/routes/stories.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from .. import database, models from ..services import stories -from ..app import safe_content_disposition +from ..utils.http import safe_content_disposition from ..database import get_db router = APIRouter() diff --git a/backend/tests/test_profile_duplicate_names.py b/backend/tests/test_profile_duplicate_names.py index 55ee8587..1a9b5672 100644 --- a/backend/tests/test_profile_duplicate_names.py +++ b/backend/tests/test_profile_duplicate_names.py @@ -12,13 +12,9 @@ from pathlib import Path from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -# Add parent directory to path to import backend modules -import sys -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from database import Base, VoiceProfile as DBVoiceProfile -from models import VoiceProfileCreate -from profiles import create_profile, update_profile +from backend.database import Base, VoiceProfile as DBVoiceProfile +from backend.models import VoiceProfileCreate +from backend.services.profiles import create_profile, update_profile @pytest.fixture diff --git a/backend/utils/http.py b/backend/utils/http.py new file mode 100644 index 00000000..d29a72bb --- /dev/null +++ b/backend/utils/http.py @@ -0,0 +1,19 @@ +"""HTTP header helpers shared by the download routes. + +Lives here rather than in ``app`` so route modules can use it without +importing the application module — ``app`` builds the FastAPI instance at +import time, which registers those same routers and closes an import cycle. +""" + +from urllib.parse import quote + + +def safe_content_disposition(disposition_type: str, filename: str) -> str: + """Build a Content-Disposition header safe for non-ASCII filenames. + + Uses RFC 5987 ``filename*`` parameter so browsers can decode UTF-8 + filenames while the ``filename`` fallback stays ASCII-only. + """ + ascii_name = "".join(c for c in filename if c.isascii() and (c.isalnum() or c in " -_.")).strip() or "download" + utf8_name = quote(filename, safe="") + return f"{disposition_type}; filename=\"{ascii_name}\"; filename*=UTF-8''{utf8_name}"