重构: 切换存储至SQLite,启用INI配置与API Key校验

This commit is contained in:
2026-02-09 17:10:11 +08:00
parent d5edbc0723
commit b9828a1b13
30 changed files with 2721 additions and 612 deletions

View File

@ -7,6 +7,39 @@ 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)
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 (10 minutes later)
conn.execute("""
UPDATE tasks
SET downloaded_at = datetime('now', '+8 hours'),
delete_after_at = datetime('now', '+8 hours', '+10 minutes')
WHERE task_id = ?
""", (task_id,))
conn.commit()
logger.info(f"Task {task_id} marked as downloaded, scheduled for deletion in 10 minutes")
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')
@ -20,6 +53,32 @@ def download_file(filename):
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)
@ -27,6 +86,7 @@ def download_file(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 '')
@ -61,7 +121,15 @@ def download_file(filename):
file_size = file_path.stat().st_size
logger.info(f"Serving file: {filename} ({file_size} bytes)")
return send_file(file_path)
# 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)