mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-12 19:34:24 +08:00
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
// 下载直链进程级缓存。
|
|
//
|
|
// 115 的 /open/ufile/downurl 换取结果带 `t=` 过期时间戳(实测为「换取时刻 +
|
|
// 1800 秒」,即 30 分钟),且换取请求是 WAF 风控重点关注的密集接口。缓存按
|
|
// pickcode 存直链、避免对同一文件反复换取;下载失败(http 403/404 等)时由
|
|
// 调用方主动清除对应条目,下一轮重试会重新换取链接。
|
|
package cloud115
|
|
|
|
import (
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// downloadURLCacheMaxTTL 直链缓存上限:仅在解析不出直链自带过期时间(`t=`)
|
|
// 时使用。115 直链实际有效 30 分钟,这里取 45 分钟只是兜底上限,正常情况下
|
|
// 由 linkExpirySafety 提前失效,避免把已经过期的直链发给客户端。
|
|
const downloadURLCacheMaxTTL = 45 * time.Minute
|
|
|
|
// linkExpirySafety 提前量:直链自身的过期时间戳到达前就停止使用,留出客户端
|
|
// 建连与传输的余量,防止出现「服务端认为还有效、CDN 已经 403」的窗口。
|
|
const linkExpirySafety = 2 * time.Minute
|
|
|
|
// maxCachedURLs 超过该数量时顺带清理过期条目,防止 map 无限膨胀。
|
|
const maxCachedURLs = 10000
|
|
|
|
type urlCacheEntry struct {
|
|
url string
|
|
createdAt time.Time
|
|
expiresAt time.Time
|
|
}
|
|
|
|
var (
|
|
urlCacheMu sync.Mutex
|
|
urlCache = map[string]urlCacheEntry{}
|
|
)
|
|
|
|
// urlCacheKey 构造缓存键名(pickCode + UA,实现直链防盗链按客户端 UA 独立缓存)。
|
|
func urlCacheKey(pickCode, ua string) string {
|
|
if ua == "" {
|
|
return pickCode
|
|
}
|
|
return pickCode + "@" + ua
|
|
}
|
|
|
|
// LinkExpiry 解析 115 直链自带的过期时间戳(`t=` 参数,单位秒)。
|
|
// 解析不出时返回零值,调用方按兜底 TTL 处理。
|
|
func LinkExpiry(rawURL string) time.Time {
|
|
u, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || u == nil {
|
|
return time.Time{}
|
|
}
|
|
raw := strings.TrimSpace(u.Query().Get("t"))
|
|
if raw == "" {
|
|
return time.Time{}
|
|
}
|
|
sec, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || sec <= 0 {
|
|
return time.Time{}
|
|
}
|
|
return time.Unix(sec, 0)
|
|
}
|
|
|
|
// cacheExpiry 计算写入缓存时应使用的过期时刻:跟随直链自身的 `t=` 并提前
|
|
// linkExpirySafety,解析不出时退回 downloadURLCacheMaxTTL。返回零值表示这条
|
|
// 直链已经过期,不应写入缓存。
|
|
func cacheExpiry(rawURL string, now time.Time) time.Time {
|
|
maxExpiry := now.Add(downloadURLCacheMaxTTL)
|
|
linkExpiry := LinkExpiry(rawURL)
|
|
if linkExpiry.IsZero() {
|
|
return maxExpiry
|
|
}
|
|
expiry := linkExpiry.Add(-linkExpirySafety)
|
|
if !expiry.After(now) {
|
|
return time.Time{}
|
|
}
|
|
if expiry.After(maxExpiry) {
|
|
return maxExpiry
|
|
}
|
|
return expiry
|
|
}
|
|
|
|
// GetDownloadURLCache 返回未过期的缓存直链;不存在或已过期返回空串。
|
|
func GetDownloadURLCache(pickCode string, uas ...string) string {
|
|
if pickCode == "" {
|
|
return ""
|
|
}
|
|
ua := ""
|
|
if len(uas) > 0 {
|
|
ua = uas[0]
|
|
}
|
|
key := urlCacheKey(pickCode, ua)
|
|
urlCacheMu.Lock()
|
|
defer urlCacheMu.Unlock()
|
|
entry, ok := urlCache[key]
|
|
if !ok || time.Now().After(entry.expiresAt) {
|
|
if ok {
|
|
delete(urlCache, key)
|
|
}
|
|
return ""
|
|
}
|
|
return entry.url
|
|
}
|
|
|
|
// SetDownloadURLCache 写入直链缓存。过期时刻跟随直链自身的 `t=`,已经过期的
|
|
// 直链直接丢弃,避免把死链缓存下来。
|
|
func SetDownloadURLCache(pickCode string, url string, uas ...string) {
|
|
if pickCode == "" || url == "" {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
expiresAt := cacheExpiry(url, now)
|
|
if expiresAt.IsZero() {
|
|
return
|
|
}
|
|
ua := ""
|
|
if len(uas) > 0 {
|
|
ua = uas[0]
|
|
}
|
|
urlCacheMu.Lock()
|
|
defer urlCacheMu.Unlock()
|
|
if len(urlCache) >= maxCachedURLs {
|
|
for k, e := range urlCache {
|
|
if now.After(e.expiresAt) {
|
|
delete(urlCache, k)
|
|
}
|
|
}
|
|
}
|
|
key := urlCacheKey(pickCode, ua)
|
|
urlCache[key] = urlCacheEntry{url: url, createdAt: now, expiresAt: expiresAt}
|
|
}
|
|
|
|
// ClearDownloadURLCache 删除指定 pickcode 的所有缓存(下载得到非 2xx 时调用)。
|
|
func ClearDownloadURLCache(pickCode string) {
|
|
if pickCode == "" {
|
|
return
|
|
}
|
|
urlCacheMu.Lock()
|
|
defer urlCacheMu.Unlock()
|
|
for k := range urlCache {
|
|
if k == pickCode || strings.HasPrefix(k, pickCode+"@") {
|
|
delete(urlCache, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClearDownloadURLCacheIfOlderThan 删除指定 pickcode 下「已经存在超过 minAge」
|
|
// 的缓存条目,返回是否删除了至少一条。
|
|
//
|
|
// 客户端上报「直链被 CDN 拒绝」时用它强制换链:既让下一次解析拿到新地址,
|
|
// 又不会让同一秒内的重试风暴把 115 换链接口打出突发。
|
|
func ClearDownloadURLCacheIfOlderThan(pickCode string, minAge time.Duration) bool {
|
|
if pickCode == "" {
|
|
return false
|
|
}
|
|
if minAge < 0 {
|
|
minAge = 0
|
|
}
|
|
now := time.Now()
|
|
removed := false
|
|
urlCacheMu.Lock()
|
|
defer urlCacheMu.Unlock()
|
|
for k, e := range urlCache {
|
|
if k != pickCode && !strings.HasPrefix(k, pickCode+"@") {
|
|
continue
|
|
}
|
|
if now.Sub(e.createdAt) < minAge {
|
|
continue
|
|
}
|
|
delete(urlCache, k)
|
|
removed = true
|
|
}
|
|
return removed
|
|
}
|