修复文件存储路径错误的BUG
This commit is contained in:
parent
9a4dd3ed0f
commit
858fc132ef
@ -46,7 +46,7 @@ DINGTALK_SUPER_ADMIN_UNION_IDS=
|
||||
|
||||
`DINGTALK_CLIENT_ID/SECRET` 填应用的 AppKey/AppSecret;`DINGTALK_CORP_ID` 填企业 CorpId;`DINGTALK_AGENT_ID` 用于标识工作台应用。当前免登接口不使用浏览器 OAuth 回调,但保留回调配置兼容旧入口。未配置免登参数时,普通浏览器仍可使用账号密码登录。
|
||||
|
||||
MySQL 建表脚本位于 `backend/schema.sql`。未配置 `DATABASE_URL` 时,开发环境会自动使用 `backend/data/app.db` 运行,便于本地快速验收;上传截图保存在 `backend/data/uploads/screenshots`。
|
||||
MySQL 建表脚本位于 `backend/schema.sql`。未配置 `DATABASE_URL` 时,开发环境会自动使用 `backend/data/app.db` 运行,便于本地快速验收。上传截图只保存在服务器文件夹中,不在数据库保留副本。服务器部署时必须通过 `SCREENSHOT_STORAGE_DIR` 指向代码发布目录之外的持久化绝对路径,例如 `/www/wwwroot/wendajia_shared/screenshots`,并对该目录单独备份。目录不存在或不可写时,上传接口会返回明确的存储错误。
|
||||
|
||||
## 问答助手大模型配置
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
DATABASE_URL=mysql+pymysql://ai_agent_test:WGzc6mKawk@49.233.179.206:3306/wendajia
|
||||
SCREENSHOT_STORAGE_DIR=data/uploads/screenshots
|
||||
DATABASE_URL=mysql+pymysql://user:password@127.0.0.1:3306/wendajia
|
||||
# 服务器建议填写代码目录之外、不会被发布覆盖的绝对路径,并单独备份。
|
||||
SCREENSHOT_STORAGE_DIR=/www/wwwroot/wendajia_shared/screenshots
|
||||
SECRET_KEY=replace-with-a-long-random-secret
|
||||
COOKIE_SECURE=false
|
||||
SESSION_HOURS=12
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
@ -7,6 +8,9 @@ from fastapi import HTTPException, Response
|
||||
from .config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _safe_extension(filename: str, content_type: str) -> str:
|
||||
extension = Path(filename or "").suffix.lower()
|
||||
if extension in {".jpg", ".jpeg", ".png", ".webp"}:
|
||||
@ -40,10 +44,23 @@ def store_screenshot(data: bytes, filename: str, content_type: str, category: st
|
||||
|
||||
|
||||
def screenshot_fields(data: bytes, filename: str, content_type: str, category: str) -> dict:
|
||||
try:
|
||||
file_path = store_screenshot(data, filename, content_type, category)
|
||||
except (OSError, ValueError) as exc:
|
||||
logger.exception(
|
||||
"Screenshot file write failed: category=%s filename=%s storage_dir=%s",
|
||||
category,
|
||||
filename,
|
||||
settings.screenshot_storage_dir,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="图片存储目录不可用,请联系管理员检查目录配置和写入权限",
|
||||
) from exc
|
||||
return {
|
||||
"filename": filename,
|
||||
"content_type": content_type,
|
||||
"file_path": store_screenshot(data, filename, content_type, category),
|
||||
"file_path": file_path,
|
||||
"image_data": None,
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ Run from the project root with::
|
||||
python backend/scripts/migrate_screenshots_to_files.py
|
||||
|
||||
Each row is committed only after the written file matches the source byte
|
||||
length and SHA-256 digest. Re-running is safe: rows with a valid file_path and
|
||||
an empty image_data value are skipped.
|
||||
length and SHA-256 digest. After verification, the database BLOB is cleared so
|
||||
screenshots exist only in the configured server storage directory.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
@ -19,7 +19,7 @@ BACKEND_ROOT = PROJECT_ROOT / "backend"
|
||||
if str(BACKEND_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
|
||||
from sqlalchemy import inspect, select, text # noqa: E402
|
||||
from sqlalchemy import inspect, or_, select, text # noqa: E402
|
||||
|
||||
from app.config import settings # noqa: E402
|
||||
from app.database import SessionLocal, engine # noqa: E402
|
||||
@ -32,8 +32,6 @@ TARGETS = (
|
||||
(AskAssignmentBatchScreenshot, "ask_assignment_batch_screenshots", "ask-batches"),
|
||||
(AskAssignmentScreenshot, "ask_assignment_screenshots", "ask-assignments"),
|
||||
)
|
||||
|
||||
|
||||
def ensure_columns() -> None:
|
||||
for _model, table, _category in TARGETS:
|
||||
columns = {column["name"]: column for column in inspect(engine).get_columns(table)}
|
||||
@ -57,7 +55,10 @@ def migrate() -> None:
|
||||
with SessionLocal() as db:
|
||||
image = db.scalar(
|
||||
select(model)
|
||||
.where(model.image_data.is_not(None))
|
||||
.where(
|
||||
model.image_data.is_not(None),
|
||||
or_(model.file_path.is_(None), model.file_path == ""),
|
||||
)
|
||||
.order_by(model.id)
|
||||
.limit(1)
|
||||
)
|
||||
@ -81,6 +82,7 @@ def migrate() -> None:
|
||||
print(f"{table}: complete rows={table_rows} bytes={table_bytes}", flush=True)
|
||||
print(f"TOTAL rows={migrated_rows} bytes={migrated_bytes}", flush=True)
|
||||
print(f"FILES {settings.screenshot_storage_dir.resolve()}", flush=True)
|
||||
print("DATABASE_COPIES CLEARED", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
44
backend/tests/test_screenshot_storage.py
Normal file
44
backend/tests/test_screenshot_storage.py
Normal file
@ -0,0 +1,44 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app import screenshot_storage
|
||||
|
||||
|
||||
def test_new_screenshot_is_stored_only_as_server_file(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(screenshot_storage.settings, "screenshot_storage_dir", tmp_path)
|
||||
|
||||
fields = screenshot_storage.screenshot_fields(
|
||||
b"image-content", "proof.png", "image/png", "answers"
|
||||
)
|
||||
|
||||
assert fields["image_data"] is None
|
||||
assert (tmp_path / fields["file_path"]).read_bytes() == b"image-content"
|
||||
|
||||
|
||||
def test_missing_file_falls_back_to_database_copy(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(screenshot_storage.settings, "screenshot_storage_dir", tmp_path)
|
||||
image = SimpleNamespace(
|
||||
file_path="answers/missing.png",
|
||||
image_data=b"database-backup",
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
assert screenshot_storage.screenshot_bytes(image) == b"database-backup"
|
||||
|
||||
|
||||
def test_file_write_failure_returns_clear_service_error(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(screenshot_storage.settings, "screenshot_storage_dir", tmp_path)
|
||||
monkeypatch.setattr(
|
||||
screenshot_storage,
|
||||
"store_screenshot",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("read-only")),
|
||||
)
|
||||
|
||||
try:
|
||||
screenshot_storage.screenshot_fields(
|
||||
b"image-content", "proof.png", "image/png", "answers"
|
||||
)
|
||||
except screenshot_storage.HTTPException as exc:
|
||||
assert exc.status_code == 503
|
||||
assert "图片存储目录不可用" in exc.detail
|
||||
else:
|
||||
raise AssertionError("expected screenshot storage error")
|
||||
Loading…
x
Reference in New Issue
Block a user