Your Name 58aed5f201 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>
2026-02-10 14:54:16 +08:00

102 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 获取私有桶文件的预签名访问 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
}