Optimize database and deployment: fix SQL injection, async auth, persist data
- Move SQLite DB to data/ directory and track in git for portability - Fix SQL injection in cleanup_old_emails (use parameterized query) - Replace sync requests with async httpx in auth.py - Enable WAL mode and foreign keys for SQLite - Add UNIQUE constraint and foreign key to account_tags table - Remove redundant indexes on primary key columns - Mount data/ volume in docker-compose for persistence - Remove unused requests dependency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
config.txt
|
||||
outlook_manager.db
|
||||
.codebuddy
|
||||
_pychache_
|
||||
__pycache__
|
||||
*.pyc
|
||||
config.txt
|
||||
.env
|
||||
# WAL临时文件不需要跟踪
|
||||
data/*.db-wal
|
||||
data/*.db-shm
|
||||
33
auth.py
33
auth.py
@@ -4,7 +4,7 @@ OAuth2认证模块
|
||||
处理Microsoft OAuth2令牌获取和刷新
|
||||
"""
|
||||
|
||||
import requests
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Optional
|
||||
from fastapi import HTTPException
|
||||
@@ -34,43 +34,42 @@ async def get_access_token(refresh_token: str, check_only: bool = False, client_
|
||||
'refresh_token': refresh_token,
|
||||
'scope': 'https://outlook.office.com/IMAP.AccessAsUser.All offline_access'
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
# 使用requests而不是httpx,因为exp.py验证有效
|
||||
response = requests.post(TOKEN_URL, data=data)
|
||||
response.raise_for_status()
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(TOKEN_URL, data=data)
|
||||
response.raise_for_status()
|
||||
|
||||
token_data = response.json()
|
||||
access_token = token_data.get('access_token')
|
||||
|
||||
|
||||
if not access_token:
|
||||
error_msg = f"获取 access_token 失败: {token_data.get('error_description', '响应中未找到 access_token')}"
|
||||
logger.error(error_msg)
|
||||
if check_only:
|
||||
return None
|
||||
raise HTTPException(status_code=401, detail=error_msg)
|
||||
|
||||
|
||||
new_refresh_token = token_data.get('refresh_token')
|
||||
if new_refresh_token and new_refresh_token != refresh_token:
|
||||
logger.debug("提示: refresh_token 已被服务器更新")
|
||||
|
||||
|
||||
return access_token
|
||||
|
||||
except requests.exceptions.HTTPError as http_err:
|
||||
|
||||
except httpx.HTTPStatusError as http_err:
|
||||
logger.error(f"请求 access_token 时发生HTTP错误: {http_err}")
|
||||
if http_err.response is not None:
|
||||
logger.error(f"服务器响应: {http_err.response.status_code} - {http_err.response.text}")
|
||||
|
||||
logger.error(f"服务器响应: {http_err.response.status_code} - {http_err.response.text}")
|
||||
|
||||
if check_only:
|
||||
return None
|
||||
raise HTTPException(status_code=401, detail="Refresh token已过期或无效,需要重新获取授权")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"请求 access_token 时发生网络错误: {e}")
|
||||
if check_only:
|
||||
return None
|
||||
raise HTTPException(status_code=500, detail="Token acquisition failed")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"解析 access_token 响应时出错: {e}")
|
||||
if check_only:
|
||||
|
||||
BIN
data/outlook_manager.db
Normal file
BIN
data/outlook_manager.db
Normal file
Binary file not shown.
23
database.py
23
database.py
@@ -18,9 +18,11 @@ logger = logging.getLogger(__name__)
|
||||
class DatabaseManager:
|
||||
"""数据库管理器"""
|
||||
|
||||
def __init__(self, db_path: str = "outlook_manager.db"):
|
||||
def __init__(self, db_path: str = "data/outlook_manager.db"):
|
||||
self.db_path = db_path
|
||||
self._local = threading.local()
|
||||
# 确保数据目录存在
|
||||
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
self.init_database()
|
||||
|
||||
def get_connection(self) -> sqlite3.Connection:
|
||||
@@ -28,6 +30,9 @@ class DatabaseManager:
|
||||
if not hasattr(self._local, 'connection'):
|
||||
self._local.connection = sqlite3.connect(self.db_path)
|
||||
self._local.connection.row_factory = sqlite3.Row
|
||||
# 启用WAL模式,提升并发读性能
|
||||
self._local.connection.execute('PRAGMA journal_mode=WAL')
|
||||
self._local.connection.execute('PRAGMA foreign_keys=ON')
|
||||
return self._local.connection
|
||||
|
||||
def init_database(self):
|
||||
@@ -51,10 +56,11 @@ class DatabaseManager:
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS account_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
tags TEXT NOT NULL, -- JSON格式存储标签数组
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (email) REFERENCES accounts(email) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
@@ -114,9 +120,8 @@ class DatabaseManager:
|
||||
cursor.execute(f"ALTER TABLE claude_payment_status ADD COLUMN {col} TEXT {default}")
|
||||
logger.info(f"已为 claude_payment_status 添加 {col} 列")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_account_tags_email ON account_tags(email)')
|
||||
# 创建索引(accounts.email 是 PRIMARY KEY,无需额外索引)
|
||||
# account_tags.email 已有 UNIQUE 约束,无需额外索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_email ON email_cache(email)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_message_id ON email_cache(message_id)')
|
||||
|
||||
@@ -309,9 +314,9 @@ class DatabaseManager:
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM email_cache
|
||||
WHERE created_at < datetime('now', '-{} days')
|
||||
'''.format(days))
|
||||
DELETE FROM email_cache
|
||||
WHERE created_at < datetime('now', ? || ' days')
|
||||
''', (f'-{days}',))
|
||||
deleted_count = cursor.rowcount
|
||||
conn.commit()
|
||||
return deleted_count
|
||||
|
||||
@@ -9,6 +9,8 @@ services:
|
||||
volumes:
|
||||
# 挂载配置文件,便于修改邮箱配置
|
||||
- ./config.txt:/app/config.txt
|
||||
# 持久化SQLite数据库,防止容器重建丢失数据
|
||||
- ./data:/app/data
|
||||
# 可选:挂载日志目录
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
|
||||
@@ -2,7 +2,6 @@ fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
httpx==0.25.2
|
||||
requests==2.31.0
|
||||
python-multipart==0.0.6
|
||||
jinja2==3.1.2
|
||||
aiofiles==23.2.1
|
||||
Reference in New Issue
Block a user