fix(反馈): 优化图片处理与静态资源缓存 #211

Merged
guke merged 3 commits from fix-bugfix0731 into main 2026-08-01 23:24:18 +08:00
Member
No description provided.
zuochenyong added 1 commit 2026-07-31 22:35:42 +08:00
guke added 1 commit 2026-08-01 23:06:51 +08:00
Member

🤖 review-pr 深审结论

🟢 看着没问题(置信度 0.85)——反馈图缩略图方案实现得比较稳:路径穿越三处设防、原子写、失败优雅回退、向后兼容、缓存作用域正确。已在合并 main 后的隔离 worktree 实跑验证,无阻断性问题,仅几条低优先建议。

实跑验证(合并结果树上,已装 PR 声明的新依赖 Pillow 12.3)

  • pytest tests/test_feedback.py4 passed
  • 自写缩略图功能测试(生成/穿越防护/懒补图/损坏图回退)→ 全过(1000×800 PNG → 256×205 JPEG)
  • 自写 HTTP 路由测试(缩略图路由、Cache-Control、404、非 feedback 静态文件加 immutable)→ 2 passed
  • ruff check 改动 4 文件 → All checks passed

🟡 低优先建议(非阻断)

  • [low-med] 事件循环阻塞save_feedback_image 现在在 async def submit_feedback 里同步做 PIL 解码+LANCZOS 缩放+JPEG 编码(最多 6 张顺序处理),会阻塞事件循环;单 worker 下会拖慢并发请求。反馈是低频路径影响有限,但可考虑 run_in_threadpool 卸载,或改 fire-and-forget(失败本就已容忍)。
  • [low] 服务端解码用户图片的 DoS 面:新增了对用户上传图的服务端解码。构造到 Pillow MAX_IMAGE_PIXELS 2× 以内(~178M px)的图可绕过炸弹保护、瞬时吃较多内存;已有 5MB 字节上限 + 鉴权兜底,可考虑显式收紧 MAX_IMAGE_PIXELS 或加尺寸上限。
  • [low] 冗余分支FeedbackMediaStaticFiles 里对 feedback_thumbs/ 的特判在正常运行下不可达——显式路由 /media/feedback_thumbs/{filename} 始终遮蔽静态挂载(缩略图恒由 FileResponse 提供并自带 immutable 头)。无害的 defense-in-depth。
  • [nit] 回退图缓存:生成失败时路由返回原图字节(可能是 PNG)但 URL 以 .jpg 结尾并按 immutable 缓存 1 年;图片按内容嗅探能正常显示,且解码失败对同一文件是确定性的,可接受。
  • [nit] main.py 顶部已 import HTTPExceptiondownload_apk 内仍有局部 from fastapi import HTTPException(历史遗留),现在冗余可删。

👍 亮点

  • 列表接口 /records 不做图片解码,feedback_thumbnail_url 是纯字符串映射——正解,避开了注释警示的 N 张图解码。
  • 路径穿越在 _feedback_thumbnail_paths / feedback_thumbnail_file 双重把关(Path(name).name != name + 后缀白名单),实测 ../a/b、反斜杠、空串、.jpg 全部拒绝。
  • 原子写(随机 temp + os.replace)并发安全、跨平台;EXIF 方向已处理;宽高比保持。
  • schema 新字段 additive + default_factory,旧记录首次查看时懒补缩略图,向后兼容。

注:本地为跑测试向共享 venv 装了 Pillow(即 PR 声明的 pillow>=11.0.0,合并后本就需要)。

## 🤖 review-pr 深审结论 🟢 **看着没问题**(置信度 0.85)——反馈图缩略图方案实现得比较稳:路径穿越三处设防、原子写、失败优雅回退、向后兼容、缓存作用域正确。已在**合并 main 后的隔离 worktree** 实跑验证,无阻断性问题,仅几条低优先建议。 ### ✅ 实跑验证(合并结果树上,已装 PR 声明的新依赖 Pillow 12.3) - `pytest tests/test_feedback.py` → **4 passed** - 自写缩略图功能测试(生成/穿越防护/懒补图/损坏图回退)→ **全过**(1000×800 PNG → 256×205 JPEG) - 自写 HTTP 路由测试(缩略图路由、Cache-Control、404、非 feedback 静态文件**不**加 immutable)→ **2 passed** - `ruff check` 改动 4 文件 → **All checks passed** ### 🟡 低优先建议(非阻断) - **[low-med] 事件循环阻塞**:`save_feedback_image` 现在在 `async def submit_feedback` 里同步做 PIL 解码+LANCZOS 缩放+JPEG 编码(最多 6 张顺序处理),会阻塞事件循环;单 worker 下会拖慢并发请求。反馈是低频路径影响有限,但可考虑 `run_in_threadpool` 卸载,或改 fire-and-forget(失败本就已容忍)。 - **[low] 服务端解码用户图片的 DoS 面**:新增了对用户上传图的服务端解码。构造到 Pillow `MAX_IMAGE_PIXELS` 2× 以内(~178M px)的图可绕过炸弹保护、瞬时吃较多内存;已有 5MB 字节上限 + 鉴权兜底,可考虑显式收紧 `MAX_IMAGE_PIXELS` 或加尺寸上限。 - **[low] 冗余分支**:`FeedbackMediaStaticFiles` 里对 `feedback_thumbs/` 的特判在正常运行下不可达——显式路由 `/media/feedback_thumbs/{filename}` 始终遮蔽静态挂载(缩略图恒由 FileResponse 提供并自带 immutable 头)。无害的 defense-in-depth。 - **[nit] 回退图缓存**:生成失败时路由返回原图字节(可能是 PNG)但 URL 以 `.jpg` 结尾并按 immutable 缓存 1 年;图片按内容嗅探能正常显示,且解码失败对同一文件是确定性的,可接受。 - **[nit]** `main.py` 顶部已 import `HTTPException`,`download_apk` 内仍有局部 `from fastapi import HTTPException`(历史遗留),现在冗余可删。 ### 👍 亮点 - 列表接口 `/records` **不做**图片解码,`feedback_thumbnail_url` 是纯字符串映射——正解,避开了注释警示的 N 张图解码。 - 路径穿越在 `_feedback_thumbnail_paths` / `feedback_thumbnail_file` 双重把关(`Path(name).name != name` + 后缀白名单),实测 `../`、`a/b`、反斜杠、空串、`.jpg` 全部拒绝。 - 原子写(随机 temp + `os.replace`)并发安全、跨平台;EXIF 方向已处理;宽高比保持。 - schema 新字段 additive + `default_factory`,旧记录首次查看时懒补缩略图,向后兼容。 > 注:本地为跑测试向共享 venv 装了 Pillow(即 PR 声明的 `pillow>=11.0.0`,合并后本就需要)。
guke added 1 commit 2026-08-01 23:23:18 +08:00
guke merged commit 08a49504fa into main 2026-08-01 23:24:18 +08:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: WonderableAI/shaguabijia-app-server#211