后端 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>
102 lines
2.5 KiB
TypeScript
102 lines
2.5 KiB
TypeScript
/**
|
||
* 获取私有桶文件的预签名访问 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
|
||
}
|