oss.py 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. """OSS 转存:把帖子媒体直链转存到公网 CDN,返回 cdn_url(绕过防盗链 / 临时签名链时效)。
  2. 接口(实测 2026-06-26):
  3. POST settings.oss_upload_url body{src_url, src_type:"video"|"image"}
  4. → {status:0, oss_object:{cdn_url:"https://res.cybertogether.net/...", oss_object_key, ...}}
  5. 纯 parser(parse_oss_response)可离线测;upload_stream 负责 HTTP(失败抛 OssError);
  6. to_oss 是兜底包装——任何失败返回原 src_url、永不抛,供 decompose 降级不阻断。
  7. """
  8. from __future__ import annotations
  9. from typing import Any, Callable, Optional
  10. import httpx
  11. from core.config import Settings
  12. class OssError(RuntimeError):
  13. pass
  14. def parse_oss_response(response: Any) -> str:
  15. """{status:0, oss_object:{cdn_url}} → cdn_url。status!=0 或缺 cdn_url 抛 OssError。"""
  16. if not isinstance(response, dict):
  17. raise OssError("bad_response: not a dict")
  18. status = response.get("status")
  19. if status not in (0, "0"):
  20. raise OssError(f"business_error: status={status} msg={response.get('msg')}")
  21. obj = response.get("oss_object") or {}
  22. cdn = obj.get("cdn_url")
  23. if not cdn:
  24. raise OssError("empty_cdn_url: oss_object.cdn_url missing")
  25. return cdn
  26. def upload_stream(
  27. src_url: str,
  28. *,
  29. src_type: str,
  30. settings: Optional[Settings] = None,
  31. http_post: Callable[..., Any] = httpx.post,
  32. env_file: str = ".env",
  33. ) -> str:
  34. """转存一条直链到 OSS,返回 cdn_url;HTTP/解析失败抛 OssError。"""
  35. settings = settings or Settings.from_env(env_file)
  36. if src_type not in ("video", "image"):
  37. raise OssError(f"bad_src_type: {src_type}")
  38. try:
  39. resp = http_post(
  40. settings.oss_upload_url,
  41. json={"src_url": src_url, "src_type": src_type},
  42. headers={"Content-Type": "application/json"},
  43. timeout=settings.oss_upload_timeout_seconds,
  44. )
  45. resp.raise_for_status()
  46. data = resp.json()
  47. except httpx.HTTPError as exc:
  48. raise OssError(f"http_error: {exc}") from exc
  49. except ValueError as exc:
  50. raise OssError("bad_json") from exc
  51. return parse_oss_response(data)
  52. def to_oss(
  53. src_url: str,
  54. src_type: str,
  55. *,
  56. settings: Optional[Settings] = None,
  57. http_post: Callable[..., Any] = httpx.post,
  58. env_file: str = ".env",
  59. ) -> str:
  60. """兜底包装:成功返回 cdn_url,任何失败(含空入参)返回原 src_url,永不抛。"""
  61. if not src_url:
  62. return src_url
  63. try:
  64. return upload_stream(src_url, src_type=src_type, settings=settings,
  65. http_post=http_post, env_file=env_file)
  66. except Exception:
  67. return src_url