storage.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. """报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。"""
  2. from __future__ import annotations
  3. from io import BytesIO
  4. from pathlib import Path
  5. from app.core.config import settings
  6. from app.core.storage import LocalStorage, MinIOStorage, get_storage, storage
  7. def normalize_storage_path(file_url: str) -> str:
  8. """将 upload 返回的完整 URL 归一化为存储相对路径(与 admin 附件 upload 一致)。"""
  9. u = (file_url or "").strip()
  10. if not u:
  11. return ""
  12. if "?" in u:
  13. u = u.split("?", 1)[0]
  14. base_url = (settings.MINIO_BASE_URL or "").rstrip("/")
  15. if base_url and u.startswith(base_url + "/"):
  16. return u[len(base_url) + 1 :]
  17. local_base = settings.LOCAL_STORAGE_URL.rstrip("/")
  18. if u.startswith(local_base + "/"):
  19. return u[len(local_base) + 1 :]
  20. if u.startswith("http://") or u.startswith("https://"):
  21. store = get_storage()
  22. if isinstance(store, MinIOStorage):
  23. return store._extract_object_key(u)
  24. return u.lstrip("/")
  25. def upload_bx_file(file_content: bytes, filename: str, *, biz_type: str = "") -> tuple[str, str]:
  26. """
  27. 上传文件到对象存储。
  28. 返回 (relative_path, access_url)。
  29. """
  30. folder = "bx"
  31. if biz_type:
  32. folder = f"bx/{biz_type.strip('/')}"
  33. full_url = storage.upload_file(file_content, filename, folder)
  34. relative_path = normalize_storage_path(full_url)
  35. return relative_path, full_url
  36. def get_access_url(file_path: str) -> str:
  37. """获取可访问 URL(本地/MinIO 公开地址)。"""
  38. store = get_storage()
  39. key = normalize_storage_path(file_path)
  40. if not key:
  41. return ""
  42. return store.get_url(key)
  43. def get_preview_url(file_path: str, *, expires_seconds: int = 3600) -> str:
  44. """图片/附件预览 URL:MinIO 预签名 inline,本地/其他存储用 get_url。"""
  45. store = get_storage()
  46. key = normalize_storage_path(file_path)
  47. if not key:
  48. return ""
  49. if isinstance(store, MinIOStorage):
  50. # 走 MinIOStorage.presigned_preview_url(按 MINIO_BASE_URL 公网域名签发)
  51. return store.presigned_preview_url(key, expires_seconds=expires_seconds)
  52. return store.get_url(key)
  53. def get_download_url(file_path: str, file_name: str, *, expires_seconds: int = 3600) -> str:
  54. """带文件名的下载 URL(对齐门户 attachment/download-url)。"""
  55. store = get_storage()
  56. key = normalize_storage_path(file_path)
  57. return store.presigned_download_url(key, file_name, expires_seconds)
  58. def download_file_bytes(file_path: str) -> bytes:
  59. """读取文件二进制,供 StreamingResponse 下载。"""
  60. store = get_storage()
  61. key = normalize_storage_path(file_path)
  62. if not key:
  63. raise ValueError("无效的文件路径")
  64. if isinstance(store, MinIOStorage):
  65. response = store.client.get_object(store.bucket_name, key)
  66. try:
  67. return response.read()
  68. finally:
  69. response.close()
  70. response.release_conn()
  71. if isinstance(store, LocalStorage):
  72. file_full_path = Path(store.upload_dir) / key
  73. if not file_full_path.is_file():
  74. raise FileNotFoundError(f"文件不存在: {key}")
  75. return file_full_path.read_bytes()
  76. # 七牛/OSS 等:回退 HTTP 拉取(开发环境较少用到)
  77. import urllib.request
  78. url = store.get_url(key)
  79. with urllib.request.urlopen(url, timeout=60) as resp:
  80. return resp.read()
  81. def file_stream(file_path: str) -> BytesIO:
  82. return BytesIO(download_file_bytes(file_path))