lolly/internal/proxy/tempfile.go
xfy d21e27fbac fix(lint): 修复 golangci-lint 错误 (119 -> 0 issues)
主要修复:
- errcheck: defer Close 使用 //nolint:errcheck,类型断言改为 ok 检查
- govet fieldalignment: 调整结构体字段顺序优化内存布局
- revive unused-parameter: 将未使用参数改为 _
- exhaustive: 添加缺失的 switch case 或 default
- goconst: 提取重复字符串为常量 (accessAllow, accessDeny 等)
- staticcheck SA9003: 修复空分支逻辑
- gofmt: 运行 gofmt -w 格式化
- nolintlint: 修复 nolint 注释格式

其他改进:
- 更新 .golangci.yml 配置,启用更严格的检查
- 移除未使用的代码和导入
- 简化测试辅助函数调用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 16:15:31 +08:00

487 lines
11 KiB
Go
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.

// Package proxy 提供反向代理功能的临时文件处理。
//
// 该文件包含代理响应的临时文件存储功能,用于保护内存:
// - 响应大小检测
// - 超过阈值时写入临时文件
// - 超过最大值时返回 502
// - 响应完成后自动清理
//
// 主要用途:
//
// 大响应场景下避免内存溢出。
//
// 注意事项:
// - 使用 bodylimit.ParseSize 解析大小字符串
// - 临时文件在响应完成后删除
// - 无 Content-Length 时动态检测
//
// 作者xfy
package proxy
import (
"fmt"
"io"
"os"
"sync"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/middleware/bodylimit"
)
// TempFileManager 临时文件管理器。
//
// 管理代理响应的临时文件存储,保护内存免受大响应影响。
//
// 注意事项:
// - 阈值和最大值的解析在初始化时完成
// - 临时文件在响应后自动删除
// - 并发安全
type TempFileManager struct {
activeFiles map[string]*TempFile
tempPath string
threshold int64
maxSize int64
mu sync.RWMutex
}
// TempFile 临时文件包装器。
//
// 包装临时文件,提供便捷的读写和自动清理功能。
type TempFile struct {
// file 底层文件句柄
file *os.File
// path 文件路径
path string
// size 当前写入大小
size int64
// maxSize 最大允许大小
maxSize int64
// exceeded 是否超过最大大小
exceeded bool
}
// NewTempFileManager 创建临时文件管理器。
//
// 参数:
// - tempPath: 临时文件存储目录
// - threshold: 触发阈值字符串(如 "1m"
// - maxSize: 最大大小字符串(如 "1024m"
//
// 返回值:
// - *TempFileManager: 临时文件管理器实例
// - error: 解析大小失败时的错误
func NewTempFileManager(tempPath, threshold, maxSize string) (*TempFileManager, error) {
// 解析阈值
thresholdBytes, err := bodylimit.ParseSize(threshold)
if err != nil {
return nil, fmt.Errorf("解析 temp_file_threshold 失败: %w", err)
}
// 解析最大大小
maxSizeBytes, err := bodylimit.ParseSize(maxSize)
if err != nil {
return nil, fmt.Errorf("解析 max_temp_file_size 失败: %w", err)
}
// 使用默认临时目录
if tempPath == "" {
tempPath = os.TempDir()
}
// 确保临时目录存在
if err := os.MkdirAll(tempPath, 0755); err != nil {
return nil, fmt.Errorf("创建临时目录失败: %w", err)
}
return &TempFileManager{
tempPath: tempPath,
threshold: thresholdBytes,
maxSize: maxSizeBytes,
activeFiles: make(map[string]*TempFile),
}, nil
}
// ShouldUseTempFile 检查是否应该使用临时文件。
//
// 参数:
// - contentLength: Content-Length 值(-1 表示未知)
//
// 返回值:
// - bool: 是否应使用临时文件
func (m *TempFileManager) ShouldUseTempFile(contentLength int64) bool {
// 如果已知大小且超过阈值,使用临时文件
if contentLength >= 0 && contentLength >= m.threshold {
return true
}
// 未知大小时由调用方决定(动态检测)
return false
}
// CreateTempFile 创建临时文件。
//
// 返回值:
// - *TempFile: 临时文件实例
// - error: 创建失败时的错误
func (m *TempFileManager) CreateTempFile() (*TempFile, error) {
// 创建临时文件
file, err := os.CreateTemp(m.tempPath, "lolly-proxy-*")
if err != nil {
return nil, fmt.Errorf("创建临时文件失败: %w", err)
}
tf := &TempFile{
file: file,
path: file.Name(),
maxSize: m.maxSize,
}
// 记录活动文件
m.mu.Lock()
m.activeFiles[tf.path] = tf
m.mu.Unlock()
return tf, nil
}
// GetThreshold 获取触发阈值。
func (m *TempFileManager) GetThreshold() int64 {
return m.threshold
}
// GetMaxSize 获取最大大小限制。
func (m *TempFileManager) GetMaxSize() int64 {
return m.maxSize
}
// RemoveTempFile 移除临时文件记录。
//
// 参数:
// - path: 临时文件路径
func (m *TempFileManager) RemoveTempFile(path string) {
m.mu.Lock()
delete(m.activeFiles, path)
m.mu.Unlock()
}
// GetActiveCount 获取活动临时文件数量。
func (m *TempFileManager) GetActiveCount() int {
m.mu.RLock()
count := len(m.activeFiles)
m.mu.RUnlock()
return count
}
// Write 写入数据到临时文件。
//
// 参数:
// - data: 要写入的数据
//
// 返回值:
// - int: 写入的字节数
// - error: 写入失败或超过最大大小时的错误
func (tf *TempFile) Write(data []byte) (int, error) {
if tf.exceeded {
return 0, fmt.Errorf("response exceeds max_temp_file_size")
}
// 检查是否超过最大大小
if tf.size+int64(len(data)) > tf.maxSize {
tf.exceeded = true
return 0, fmt.Errorf("response exceeds max_temp_file_size")
}
n, err := tf.file.Write(data)
tf.size += int64(n)
return n, err
}
// WriteTo 将临时文件内容写入响应。
//
// 参数:
// - ctx: fasthttp 请求上下文
// - statusCode: HTTP 状态码
//
// 返回值:
// - error: 写入失败时的错误
func (tf *TempFile) WriteTo(ctx *fasthttp.RequestCtx, statusCode int) error {
// 关闭文件以便读取
if err := tf.file.Close(); err != nil {
return fmt.Errorf("关闭临时文件失败: %w", err)
}
// 重新打开文件用于读取
file, err := os.Open(tf.path)
if err != nil {
return fmt.Errorf("打开临时文件失败: %w", err)
}
defer func() {
_ = file.Close()
}()
// 设置状态码
ctx.Response.SetStatusCode(statusCode)
// 流式传输文件内容
buf := make([]byte, 64*1024) // 64KB 缓冲区
for {
n, err := file.Read(buf)
if n > 0 {
ctx.Response.AppendBody(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("读取临时文件失败: %w", err)
}
}
return nil
}
// Close 关闭并删除临时文件。
func (tf *TempFile) Close() error {
if tf.file != nil {
_ = tf.file.Close()
}
if tf.path != "" {
_ = os.Remove(tf.path)
}
return nil
}
// IsExceeded 检查是否超过最大大小。
func (tf *TempFile) IsExceeded() bool {
return tf.exceeded
}
// GetSize 获取当前写入大小。
func (tf *TempFile) GetSize() int64 {
return tf.size
}
// GetPath 获取临时文件路径。
func (tf *TempFile) GetPath() string {
return tf.path
}
// DynamicTempFileWriter 动态检测临时文件写入器。
//
// 用于未知 Content-Length 时的动态阈值检测。
type DynamicTempFileWriter struct {
// manager 临时文件管理器
manager *TempFileManager
// tempFile 当前使用的临时文件(如果已切换)
tempFile *TempFile
// buffer 缓冲区(在未超过阈值前使用)
buffer []byte
// totalSize 已接收的总大小
totalSize int64
// threshold 触发阈值
threshold int64
// maxSize 最大大小限制
maxSize int64
// switched 是否已切换到临时文件
switched bool
// exceeded 是否超过最大大小
exceeded bool
}
// NewDynamicTempFileWriter 创建动态临时文件写入器。
//
// 参数:
// - manager: 临时文件管理器
//
// 返回值:
// - *DynamicTempFileWriter: 动态写入器实例
func NewDynamicTempFileWriter(manager *TempFileManager) *DynamicTempFileWriter {
return &DynamicTempFileWriter{
manager: manager,
buffer: make([]byte, 0, manager.GetThreshold()),
threshold: manager.GetThreshold(),
maxSize: manager.GetMaxSize(),
}
}
// Write 写入数据。
//
// 参数:
// - data: 要写入的数据
//
// 返回值:
// - error: 写入失败或超过最大大小时的错误
func (w *DynamicTempFileWriter) Write(data []byte) error {
if w.exceeded {
return fmt.Errorf("response exceeds max_temp_file_size")
}
dataLen := int64(len(data))
// 检查是否超过最大大小
if w.totalSize+dataLen > w.maxSize {
w.exceeded = true
return fmt.Errorf("response exceeds max_temp_file_size")
}
// 如果已经切换到临时文件,直接写入
if w.switched {
_, err := w.tempFile.Write(data)
return err
}
// 检查是否需要切换到临时文件
if w.totalSize+dataLen >= w.threshold {
// 创建临时文件
tf, err := w.manager.CreateTempFile()
if err != nil {
return err
}
w.tempFile = tf
w.switched = true
// 将缓冲区数据写入临时文件
if len(w.buffer) > 0 {
_, writeErr := w.tempFile.Write(w.buffer)
if writeErr != nil {
return writeErr
}
w.buffer = nil // 释放缓冲区
}
// 写入新数据
_, err = w.tempFile.Write(data)
if err != nil {
return err
}
} else {
// 继续累积到缓冲区
w.buffer = append(w.buffer, data...)
}
w.totalSize += dataLen
return nil
}
// Finalize 完成写入并返回结果。
//
// 参数:
// - ctx: fasthttp 请求上下文
// - statusCode: HTTP 状态码
//
// 返回值:
// - error: 处理失败时的错误
func (w *DynamicTempFileWriter) Finalize(ctx *fasthttp.RequestCtx, statusCode int) error {
if w.exceeded {
return fmt.Errorf("response exceeds max_temp_file_size")
}
// 设置状态码
ctx.Response.SetStatusCode(statusCode)
// 如果使用了临时文件,流式传输
if w.switched && w.tempFile != nil {
// 关闭文件以便读取
if err := w.tempFile.file.Close(); err != nil {
return fmt.Errorf("关闭临时文件失败: %w", err)
}
// 重新打开文件用于读取
file, err := os.Open(w.tempFile.path)
if err != nil {
return fmt.Errorf("打开临时文件失败: %w", err)
}
defer func() {
_ = file.Close()
}()
// 流式传输文件内容
buf := make([]byte, 64*1024) // 64KB 缓冲区
for {
n, err := file.Read(buf)
if n > 0 {
ctx.Response.AppendBody(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
_ = file.Close()
return fmt.Errorf("读取临时文件失败: %w", err)
}
}
_ = file.Close()
// 删除临时文件
_ = os.Remove(w.tempFile.path)
w.manager.RemoveTempFile(w.tempFile.path)
return nil
}
// 未使用临时文件,直接返回缓冲区内容
ctx.Response.SetBody(w.buffer)
return nil
}
// IsExceeded 检查是否超过最大大小。
func (w *DynamicTempFileWriter) IsExceeded() bool {
return w.exceeded
}
// GetTotalSize 获取总大小。
func (w *DynamicTempFileWriter) GetTotalSize() int64 {
return w.totalSize
}
// Cleanup 清理资源。
func (w *DynamicTempFileWriter) Cleanup() {
if w.tempFile != nil {
_ = w.tempFile.Close()
if w.tempFile.path != "" {
w.manager.RemoveTempFile(w.tempFile.path)
}
w.tempFile = nil
}
w.buffer = nil
}
// defaultTempFileManager 默认临时文件管理器(未配置时使用)。
var defaultTempFileManager *TempFileManager
var defaultTempFileManagerOnce sync.Once
// GetDefaultTempFileManager 获取默认临时文件管理器。
//
// 默认配置:
// - temp_path: 系统临时目录
// - temp_file_threshold: 1m
// - max_temp_file_size: 1024m
//
// 返回值:
// - *TempFileManager: 默认临时文件管理器
func GetDefaultTempFileManager() *TempFileManager {
defaultTempFileManagerOnce.Do(func() {
manager, err := NewTempFileManager("", "1m", "1024m")
if err != nil {
// 默认配置不应该出错,但如果出错则创建一个最小配置
manager = &TempFileManager{
tempPath: os.TempDir(),
threshold: 1 << 20, // 1MB
maxSize: 1 << 30, // 1GB
activeFiles: make(map[string]*TempFile),
}
}
defaultTempFileManager = manager
})
return defaultTempFileManager
}