100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
"""报销附件存储适配 — 复用门户 app.core.storage,对外保持 bx 协议。"""
|
||
from __future__ import annotations
|
||
|
||
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):
|
||
# 走 MinIOStorage.presigned_preview_url(按 MINIO_BASE_URL 公网域名签发)
|
||
return store.presigned_preview_url(key, expires_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))
|