""" Download Blueprint Handles file download endpoints. """ from pathlib import Path from flask import Blueprint, send_file, current_app from ..shared import _format_response, log_performance, logger from ..auth import require_api_key def _mark_task_downloaded(task_id): """Mark task as downloaded and schedule deletion in database.""" import sqlite3 from pathlib import Path # Use independent database connection (not from flask.g which may be closed) from ..db import get_db_path as get_config_db_path db_path = get_config_db_path(current_app) # Get cleanup age for successful tasks from config (in seconds) successful_task_cleanup_age = current_app.config.get('SUCCESSFUL_TASK_CLEANUP_AGE', 3600) try: conn = sqlite3.connect(str(db_path), check_same_thread=False) conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA busy_timeout=3000") # Update downloaded timestamp and set deletion time based on config conn.execute(""" UPDATE tasks SET downloaded_at = datetime('now', '+8 hours'), delete_after_at = datetime('now', '+8 hours', '+' || ? || ' seconds') WHERE task_id = ? """, (successful_task_cleanup_age, task_id)) conn.commit() logger.info(f"Task {task_id} marked as downloaded, scheduled for deletion in {successful_task_cleanup_age} seconds") except Exception as e: logger.error(f"Failed to mark task {task_id} as downloaded: {str(e)}", exc_info=True) finally: if 'conn' in locals(): conn.close() # Create blueprint download_bp = Blueprint('download', __name__, url_prefix='/download') @download_bp.route('/') @log_performance def download_file(filename): """Download a processed file.""" from flask import request logger.info(f"Download request for file: {filename} from IP {request.remote_addr}") # Check API key from header or query parameter provided_key = request.headers.get('X-API-Key') or request.args.get('api_key') if not provided_key: logger.warning(f"API key missing from request: POST /download/{filename}") return _format_response(401, "API key required") # Validate API key from ..auth import verify_api_key, get_db db = get_db() row = db.execute( "SELECT key_hash, salt, revoked FROM api_keys WHERE key_id = ?", (provided_key,) ).fetchone() if not row: logger.warning(f"Unknown API key used: POST /download/{filename}") return _format_response(401, "Invalid API key") if row['revoked']: logger.warning(f"Revoked API key used: POST /download/{filename}") return _format_response(401, "API key revoked") if not verify_api_key(provided_key, row['key_hash'], row['salt']): logger.warning(f"Invalid API key hash: POST /download/{filename}") return _format_response(401, "Invalid API key") try: # 支持两种路径格式: # 1. 绝对路径(以 / 开头,如 /full/path/to/file) # 2. 相对路径(task_id/filename) if filename.startswith('/'): # 绝对路径 - 直接使用 file_path = Path(filename) task_id = None # Can't determine task_id from absolute path else: # 相对路径 - 相对于 OUTPUT_FOLDER output_folder = Path(current_app.config.get('OUTPUT_FOLDER') or '') if not output_folder: logger.error("OUTPUT_FOLDER not configured") return _format_response(500, "服务器配置错误") # 解析 task_id/filename 格式 parts = filename.split('/', 1) if len(parts) != 2: logger.warning(f"Invalid relative path format: {filename}") return _format_response(400, "无效的文件路径") task_id, filename_part = parts file_path = output_folder / task_id / filename_part # Security check - ensure file is within output folder file_path = file_path.resolve() output_folder = Path(current_app.config.get('OUTPUT_FOLDER') or '').resolve() if output_folder and not str(file_path).startswith(str(output_folder)): logger.warning(f"Security violation: Attempted to access file outside output folder: {filename}") return _format_response(403, "访问被拒绝") if not file_path.exists(): logger.warning(f"File not found: {filename}") return _format_response(404, "文件未找到") if not file_path.is_file(): logger.warning(f"Path is not a file: {filename}") return _format_response(400, "不是文件") file_size = file_path.stat().st_size logger.info(f"Serving file: {filename} ({file_size} bytes)") # Mark download immediately before sending file if task_id: try: _mark_task_downloaded(task_id) except Exception as e: logger.error(f"Failed to mark download for task {task_id}: {str(e)}") response = send_file(file_path) return response except Exception as e: logger.error(f"Error serving file {filename}: {str(e)}", exc_info=True) return _format_response(500, "内部服务器错误")