This commit is contained in:
Kevin Wong
2026-01-27 16:52:40 +08:00
parent c6c4b2313f
commit ee8cb9cfd2
14 changed files with 1307 additions and 217 deletions

View File

@@ -0,0 +1,148 @@
from supabase import Client
from app.core.supabase import get_supabase
from app.core.config import settings
from loguru import logger
from typing import Optional, Union, Dict, List, Any
from pathlib import Path
import asyncio
import functools
import os
# Supabase Storage 本地存储根目录
SUPABASE_STORAGE_LOCAL_PATH = Path("/home/rongye/ProgramFiles/Supabase/volumes/storage/stub/stub")
class StorageService:
def __init__(self):
self.supabase: Client = get_supabase()
self.BUCKET_MATERIALS = "materials"
self.BUCKET_OUTPUTS = "outputs"
def _convert_to_public_url(self, url: str) -> str:
"""将内部 URL 转换为公网可访问的 URL"""
if settings.SUPABASE_PUBLIC_URL and settings.SUPABASE_URL:
# 去掉末尾斜杠进行替换
internal_url = settings.SUPABASE_URL.rstrip('/')
public_url = settings.SUPABASE_PUBLIC_URL.rstrip('/')
return url.replace(internal_url, public_url)
return url
def get_local_file_path(self, bucket: str, path: str) -> Optional[str]:
"""
获取 Storage 文件的本地磁盘路径
Supabase Storage 文件存储结构:
{STORAGE_ROOT}/{bucket}/{path}/{internal_uuid}
Returns:
本地文件路径,如果不存在返回 None
"""
try:
# 构建目录路径
dir_path = SUPABASE_STORAGE_LOCAL_PATH / bucket / path
if not dir_path.exists():
logger.warning(f"Storage 目录不存在: {dir_path}")
return None
# 目录下只有一个文件internal_uuid
files = list(dir_path.iterdir())
if not files:
logger.warning(f"Storage 目录为空: {dir_path}")
return None
local_path = str(files[0])
logger.info(f"获取本地文件路径: {local_path}")
return local_path
except Exception as e:
logger.error(f"获取本地文件路径失败: {e}")
return None
async def upload_file(self, bucket: str, path: str, file_data: bytes, content_type: str) -> str:
"""
异步上传文件到 Supabase Storage
"""
try:
# 运行在线程池中,避免阻塞事件循环
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None,
functools.partial(
self.supabase.storage.from_(bucket).upload,
path=path,
file=file_data,
file_options={"content-type": content_type, "upsert": "true"}
)
)
logger.info(f"Storage upload success: {path}")
return path
except Exception as e:
logger.error(f"Storage upload failed: {e}")
raise e
async def get_signed_url(self, bucket: str, path: str, expires_in: int = 3600) -> str:
"""异步获取签名访问链接"""
try:
loop = asyncio.get_running_loop()
res = await loop.run_in_executor(
None,
lambda: self.supabase.storage.from_(bucket).create_signed_url(path, expires_in)
)
# 兼容处理
url = ""
if isinstance(res, dict) and "signedURL" in res:
url = res["signedURL"]
elif isinstance(res, str):
url = res
else:
logger.warning(f"Unexpected signed_url response: {res}")
url = res.get("signedURL", "") if isinstance(res, dict) else str(res)
# 转换为公网可访问的 URL
return self._convert_to_public_url(url)
except Exception as e:
logger.error(f"Get signed URL failed: {e}")
return ""
async def get_public_url(self, bucket: str, path: str) -> str:
"""获取公开访问链接"""
try:
loop = asyncio.get_running_loop()
res = await loop.run_in_executor(
None,
lambda: self.supabase.storage.from_(bucket).get_public_url(path)
)
# 转换为公网可访问的 URL
return self._convert_to_public_url(res)
except Exception as e:
logger.error(f"Get public URL failed: {e}")
return ""
async def delete_file(self, bucket: str, path: str):
"""异步删除文件"""
try:
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None,
lambda: self.supabase.storage.from_(bucket).remove([path])
)
logger.info(f"Deleted file: {bucket}/{path}")
except Exception as e:
logger.error(f"Delete file failed: {e}")
pass
async def list_files(self, bucket: str, path: str) -> List[Any]:
"""异步列出文件"""
try:
loop = asyncio.get_running_loop()
res = await loop.run_in_executor(
None,
lambda: self.supabase.storage.from_(bucket).list(path)
)
return res or []
except Exception as e:
logger.error(f"List files failed: {e}")
return []
storage_service = StorageService()