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
|
config.txt
|
||||||
outlook_manager.db
|
|
||||||
.codebuddy
|
.codebuddy
|
||||||
_pychache_
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
config.txt
|
.env
|
||||||
|
# WAL临时文件不需要跟踪
|
||||||
|
data/*.db-wal
|
||||||
|
data/*.db-shm
|
||||||
11
auth.py
11
auth.py
@@ -4,7 +4,7 @@ OAuth2认证模块
|
|||||||
处理Microsoft OAuth2令牌获取和刷新
|
处理Microsoft OAuth2令牌获取和刷新
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import httpx
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -36,8 +36,8 @@ async def get_access_token(refresh_token: str, check_only: bool = False, client_
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用requests而不是httpx,因为exp.py验证有效
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
response = requests.post(TOKEN_URL, data=data)
|
response = await client.post(TOKEN_URL, data=data)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
token_data = response.json()
|
token_data = response.json()
|
||||||
@@ -56,16 +56,15 @@ async def get_access_token(refresh_token: str, check_only: bool = False, client_
|
|||||||
|
|
||||||
return access_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}")
|
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:
|
if check_only:
|
||||||
return None
|
return None
|
||||||
raise HTTPException(status_code=401, detail="Refresh token已过期或无效,需要重新获取授权")
|
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}")
|
logger.error(f"请求 access_token 时发生网络错误: {e}")
|
||||||
if check_only:
|
if check_only:
|
||||||
return None
|
return None
|
||||||
|
|||||||
BIN
data/outlook_manager.db
Normal file
BIN
data/outlook_manager.db
Normal file
Binary file not shown.
21
database.py
21
database.py
@@ -18,9 +18,11 @@ logger = logging.getLogger(__name__)
|
|||||||
class DatabaseManager:
|
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.db_path = db_path
|
||||||
self._local = threading.local()
|
self._local = threading.local()
|
||||||
|
# 确保数据目录存在
|
||||||
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
self.init_database()
|
self.init_database()
|
||||||
|
|
||||||
def get_connection(self) -> sqlite3.Connection:
|
def get_connection(self) -> sqlite3.Connection:
|
||||||
@@ -28,6 +30,9 @@ class DatabaseManager:
|
|||||||
if not hasattr(self._local, 'connection'):
|
if not hasattr(self._local, 'connection'):
|
||||||
self._local.connection = sqlite3.connect(self.db_path)
|
self._local.connection = sqlite3.connect(self.db_path)
|
||||||
self._local.connection.row_factory = sqlite3.Row
|
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
|
return self._local.connection
|
||||||
|
|
||||||
def init_database(self):
|
def init_database(self):
|
||||||
@@ -51,10 +56,11 @@ class DatabaseManager:
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS account_tags (
|
CREATE TABLE IF NOT EXISTS account_tags (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL UNIQUE,
|
||||||
tags TEXT NOT NULL, -- JSON格式存储标签数组
|
tags TEXT NOT NULL, -- JSON格式存储标签数组
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
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}")
|
cursor.execute(f"ALTER TABLE claude_payment_status ADD COLUMN {col} TEXT {default}")
|
||||||
logger.info(f"已为 claude_payment_status 添加 {col} 列")
|
logger.info(f"已为 claude_payment_status 添加 {col} 列")
|
||||||
|
|
||||||
# 创建索引
|
# 创建索引(accounts.email 是 PRIMARY KEY,无需额外索引)
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email)')
|
# account_tags.email 已有 UNIQUE 约束,无需额外索引
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_account_tags_email ON account_tags(email)')
|
|
||||||
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_email ON email_cache(email)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_message_id ON email_cache(message_id)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_message_id ON email_cache(message_id)')
|
||||||
|
|
||||||
@@ -310,8 +315,8 @@ class DatabaseManager:
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
DELETE FROM email_cache
|
DELETE FROM email_cache
|
||||||
WHERE created_at < datetime('now', '-{} days')
|
WHERE created_at < datetime('now', ? || ' days')
|
||||||
'''.format(days))
|
''', (f'-{days}',))
|
||||||
deleted_count = cursor.rowcount
|
deleted_count = cursor.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return deleted_count
|
return deleted_count
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
# 挂载配置文件,便于修改邮箱配置
|
# 挂载配置文件,便于修改邮箱配置
|
||||||
- ./config.txt:/app/config.txt
|
- ./config.txt:/app/config.txt
|
||||||
|
# 持久化SQLite数据库,防止容器重建丢失数据
|
||||||
|
- ./data:/app/data
|
||||||
# 可选:挂载日志目录
|
# 可选:挂载日志目录
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ fastapi==0.104.1
|
|||||||
uvicorn[standard]==0.24.0
|
uvicorn[standard]==0.24.0
|
||||||
pydantic==2.5.0
|
pydantic==2.5.0
|
||||||
httpx==0.25.2
|
httpx==0.25.2
|
||||||
requests==2.31.0
|
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
Reference in New Issue
Block a user