File size: 3,086 Bytes
6d60378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// device_token.go X-Device-Token 的文件兜底读取(与桌面端共用状态文件)。
//
// 容器内无桌面端 Turing Shield SDK,无法像 Python fork(xiaofan6ya/converter.py)那样
// 现取 token。这里提供另一条路径:宿主把桌面端生成的 device token 落 /app/data/device_token
// (或任意挂载路径),网关定期读取注入。读取频率限 5 分钟一次缓存,>1KB 或读失败则忽略
// (优雅降级不注入,不影响主流程)。
package upstream

import (
	"errors"
	"os"
	"strings"
	"sync"
	"time"
)

var errDeviceTokenTooLarge = errors.New("device token file too large")

// deviceTokenFile 文件读取缓存 TTL(秒)。桌面端 SDK 自身也有缓存,这里再兜一层
// 避免每次出站请求都 stat+read 文件。
const deviceTokenFileTTL = 5 * time.Minute

// deviceTokenFileMaxLen token 文件最大字节数。token 通常几百字节;超过 1KB
// 视为异常(非 token 内容 / 文件被误用),忽略不注入。
const deviceTokenFileMaxLen = 1024

// deviceTokenFileCache 缓存 device token 文件读取结果(path → token+读取时刻)。
type deviceTokenFileCache struct {
	mu      sync.Mutex
	path    string
	token   string
	readAt  time.Time
	lastErr error
}

var dtFileCache = &deviceTokenFileCache{}

// readDeviceTokenFile 读取并缓存 device token 文件;5 分钟内复用上次结果。
// 返回空串表示无可用 token(文件未配置 / 读失败 / 内容过长 / 空白)。
func readDeviceTokenFile(path string) string {
	if path == "" {
		return ""
	}
	// 快路径:缓存命中且未过期,直接返回缓存值。注意全程持锁(defer Unlock)——
	// 含 5 分钟一次的过期重读(锁内读文件)。调用频率极低(每 5min 最多一次
	// 文件 IO,文件上限 1KB),锁内 IO 可接受;若未来出现 NFS 挂载 + 高并发的
	// 部署形态,再上 singleflight 包住重读段(YAGNI,现在不做)。
	dtFileCache.mu.Lock()
	defer dtFileCache.mu.Unlock()
	if path == dtFileCache.path && time.Since(dtFileCache.readAt) < deviceTokenFileTTL {
		return dtFileCache.token
	}
	// 缓存未命中或过期:重新读文件。
	dtFileCache.path = path
	tok, err := readTrimmedFile(path, deviceTokenFileMaxLen)
	if err != nil {
		// 读失败:清空缓存 token,避免注入过期/错误的值。
		dtFileCache.token = ""
		dtFileCache.lastErr = err
		dtFileCache.readAt = time.Now()
		return ""
	}
	dtFileCache.token = tok
	dtFileCache.lastErr = nil
	dtFileCache.readAt = time.Now()
	return tok
}

// readTrimmedFile 读文件并 trim 首尾空白,超过 maxLen 返回错误(拒绝过长内容)。
func readTrimmedFile(path string, maxLen int) (string, error) {
	info, err := os.Stat(path)
	if err != nil {
		return "", err
	}
	if info.Size() > int64(maxLen) {
		return "", errDeviceTokenTooLarge
	}
	raw, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(raw)), nil
}