| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- """报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。"""
- from __future__ import annotations
- from datetime import timedelta
- from io import BytesIO
- from pathlib import Path
- from app.core.config import settings
- from app.core.storage import LocalStorage, MinIOStorage, get_storage, storage
- def normalize_storage_path(file_url: str) -> str:
- """将 upload 返回的完整 URL 归一化为存储相对路径(与 admin 附件 upload 一致)。"""
- u = (file_url or "").strip()
- if not u:
- return ""
- if "?" in u:
- u = u.split("?", 1)[0]
- base_url = (settings.MINIO_BASE_URL or "").rstrip("/")
- if base_url and u.startswith(base_url + "/"):
- return u[len(base_url) + 1 :]
- local_base = settings.LOCAL_STORAGE_URL.rstrip("/")
- if u.startswith(local_base + "/"):
- return u[len(local_base) + 1 :]
- if u.startswith("http://") or u.startswith("https://"):
- store = get_storage()
- if isinstance(store, MinIOStorage):
- return store._extract_object_key(u)
- return u.lstrip("/")
- def upload_bx_file(file_content: bytes, filename: str, *, biz_type: str = "") -> tuple[str, str]:
- """
- 上传文件到对象存储。
- 返回 (relative_path, access_url)。
- """
- folder = "bx"
- if biz_type:
- folder = f"bx/{biz_type.strip('/')}"
- full_url = storage.upload_file(file_content, filename, folder)
- relative_path = normalize_storage_path(full_url)
- return relative_path, full_url
- def get_access_url(file_path: str) -> str:
- """获取可访问 URL(本地/MinIO 公开地址)。"""
- store = get_storage()
- key = normalize_storage_path(file_path)
- if not key:
- return ""
- return store.get_url(key)
- def get_preview_url(file_path: str, *, expires_seconds: int = 3600) -> str:
- """图片/附件预览 URL:MinIO 预签名 inline,本地/其他存储用 get_url。"""
- store = get_storage()
- key = normalize_storage_path(file_path)
- if not key:
- return ""
- if isinstance(store, MinIOStorage):
- return store.client.presigned_get_object(
- bucket_name=store.bucket_name,
- object_name=key,
- expires=timedelta(seconds=expires_seconds),
- )
- return store.get_url(key)
- def get_download_url(file_path: str, file_name: str, *, expires_seconds: int = 3600) -> str:
- """带文件名的下载 URL(对齐门户 attachment/download-url)。"""
- store = get_storage()
- key = normalize_storage_path(file_path)
- return store.presigned_download_url(key, file_name, expires_seconds)
- def download_file_bytes(file_path: str) -> bytes:
- """读取文件二进制,供 StreamingResponse 下载。"""
- store = get_storage()
- key = normalize_storage_path(file_path)
- if not key:
- raise ValueError("无效的文件路径")
- if isinstance(store, MinIOStorage):
- response = store.client.get_object(store.bucket_name, key)
- try:
- return response.read()
- finally:
- response.close()
- response.release_conn()
- if isinstance(store, LocalStorage):
- file_full_path = Path(store.upload_dir) / key
- if not file_full_path.is_file():
- raise FileNotFoundError(f"文件不存在: {key}")
- return file_full_path.read_bytes()
- # 七牛/OSS 等:回退 HTTP 拉取(开发环境较少用到)
- import urllib.request
- url = store.get_url(key)
- with urllib.request.urlopen(url, timeout=60) as resp:
- return resp.read()
- def file_stream(file_path: str) -> BytesIO:
- return BytesIO(download_file_bytes(file_path))
|