Refactor podcast media storage to lazy tenant resolver

This commit is contained in:
ي
2026-03-12 14:59:03 +05:30
parent bc49329ed6
commit d4baf8828e
4 changed files with 133 additions and 97 deletions

View File

@@ -20,7 +20,8 @@ from api.story_writer.utils.auth import require_authenticated_user
from utils.asset_tracker import save_asset_to_library
from models.story_models import StoryAudioResult
from loguru import logger
from ..constants import PODCAST_AUDIO_DIR, audio_service
from ..constants import get_podcast_audio_service, get_podcast_media_dir
from ..utils import _resolve_podcast_media_file
from ..models import (
PodcastAudioRequest,
PodcastAudioResponse,
@@ -62,7 +63,8 @@ async def upload_podcast_audio(
file_ext = Path(file.filename).suffix or '.mp3'
unique_id = str(uuid.uuid4())[:8]
audio_filename = f"audio_{project_id or 'temp'}_{unique_id}{file_ext}"
audio_path = PODCAST_AUDIO_DIR / audio_filename
audio_base_dir = get_podcast_media_dir("audio", user_id, ensure_exists=True)
audio_path = audio_base_dir / audio_filename
# Save file
with open(audio_path, "wb") as f:
@@ -123,6 +125,7 @@ async def generate_podcast_audio(
raise HTTPException(status_code=400, detail="Text is required")
try:
audio_service = get_podcast_audio_service(user_id)
result: StoryAudioResult = audio_service.generate_ai_audio(
scene_number=0,
scene_title=request.scene_title,
@@ -267,12 +270,7 @@ async def combine_podcast_audio(
continue
# Podcast audio files are stored in podcast_audio directory
audio_path = (PODCAST_AUDIO_DIR / filename).resolve()
# Security check: ensure path is within PODCAST_AUDIO_DIR
if not str(audio_path).startswith(str(PODCAST_AUDIO_DIR)):
logger.error(f"[Podcast] Attempted path traversal when resolving audio: {audio_url}")
continue
audio_path = _resolve_podcast_media_file(filename, "audio", user_id)
else:
logger.warning(f"[Podcast] Non-API URL format, treating as direct path: {audio_url}")
audio_path = Path(audio_url)
@@ -303,7 +301,8 @@ async def combine_podcast_audio(
# Generate output filename
output_filename = f"podcast_combined_{request.project_id}_{uuid.uuid4().hex[:8]}.mp3"
output_path = PODCAST_AUDIO_DIR / output_filename
audio_base_dir = get_podcast_media_dir("audio", user_id, ensure_exists=True)
output_path = audio_base_dir / output_filename
# Write combined audio file
combined_audio.write_audiofile(
@@ -382,20 +381,13 @@ async def serve_podcast_audio(
Supports authentication via Authorization header or token query parameter.
Query parameter is useful for HTML elements like <audio> that cannot send custom headers.
"""
require_authenticated_user(current_user)
# Security check: ensure filename doesn't contain path traversal
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
audio_path = (PODCAST_AUDIO_DIR / filename).resolve()
# Security check: ensure path is within PODCAST_AUDIO_DIR
if not str(audio_path).startswith(str(PODCAST_AUDIO_DIR)):
raise HTTPException(status_code=403, detail="Access denied")
if not audio_path.exists():
raise HTTPException(status_code=404, detail="Audio file not found")
user_id = require_authenticated_user(current_user)
audio_path = _resolve_podcast_media_file(filename, "audio", user_id)
return FileResponse(audio_path, media_type="audio/mpeg")