feat: 私有桶签名 URL 支持 + TOS 凭证配置
后端 oss.py 新增 generate_presigned_url (TOS V4 Query String Auth), upload.py 新增 GET /upload/sign-url 端点。前端 api.ts 添加 getSignedUrl 方法,新增 useSignedUrl hook 支持自动缓存和过期刷新。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2f24dcfd34
commit
58aed5f201
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 获取私有桶文件的预签名访问 URL
|
||||
*
|
||||
* 用于展示/下载 TOS 私有桶中的文件。
|
||||
* 自动缓存签名 URL,过期前 5 分钟刷新。
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import { USE_MOCK } from '@/contexts/AuthContext'
|
||||
|
||||
const urlCache = new Map<string, { signedUrl: string; expireAt: number }>()
|
||||
|
||||
export function useSignedUrl(originalUrl: string | undefined | null) {
|
||||
const [signedUrl, setSignedUrl] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const mountedRef = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
return () => { mountedRef.current = false }
|
||||
}, [])
|
||||
|
||||
const fetchSignedUrl = useCallback(async () => {
|
||||
if (!originalUrl) {
|
||||
setSignedUrl(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Mock 模式直接返回原始 URL
|
||||
if (USE_MOCK) {
|
||||
setSignedUrl(originalUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// 非 TOS URL(如外部链接)直接返回
|
||||
if (!originalUrl.includes('tos-cn-') && !originalUrl.includes('volces.com') && !originalUrl.startsWith('uploads/')) {
|
||||
setSignedUrl(originalUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查缓存(提前 5 分钟过期)
|
||||
const cached = urlCache.get(originalUrl)
|
||||
if (cached && cached.expireAt > Date.now() + 5 * 60 * 1000) {
|
||||
setSignedUrl(cached.signedUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const expireSeconds = 3600
|
||||
const url = await api.getSignedUrl(originalUrl, expireSeconds)
|
||||
if (mountedRef.current) {
|
||||
setSignedUrl(url)
|
||||
urlCache.set(originalUrl, {
|
||||
signedUrl: url,
|
||||
expireAt: Date.now() + expireSeconds * 1000,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// 签名失败时回退到原始 URL
|
||||
if (mountedRef.current) {
|
||||
setSignedUrl(originalUrl)
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [originalUrl])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignedUrl()
|
||||
}, [fetchSignedUrl])
|
||||
|
||||
return { signedUrl, loading, refresh: fetchSignedUrl }
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取签名 URL 的工具函数
|
||||
*/
|
||||
export async function getSignedUrls(urls: string[]): Promise<Map<string, string>> {
|
||||
const result = new Map<string, string>()
|
||||
|
||||
if (USE_MOCK) {
|
||||
urls.forEach(u => result.set(u, u))
|
||||
return result
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
const signed = await api.getSignedUrl(url)
|
||||
result.set(url, signed)
|
||||
} catch {
|
||||
result.set(url, url)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user