// Package compression 提供 gzip_static 预压缩文件支持。 // // 该文件实现预压缩文件的检测和发送,避免实时压缩开销。 // // 主要用途: // // 用于发送预压缩的 .gz 文件,减少服务器 CPU 开销,提升响应速度。 // // 使用场景: // - 静态资源预先压缩(如 CSS、JS、HTML 文件) // - 构建时生成 .gz 文件 // - 运行时直接发送预压缩文件 // // 作者:xfy package compression import ( "os" "path/filepath" "strings" "github.com/valyala/fasthttp" "rua.plus/lolly/internal/mimeutil" ) // GzipStatic 预压缩文件支持。 // // 检查是否存在预压缩的 .gz 或 .br 文件,如果存在且客户端支持对应编码, // 则直接发送,避免实时压缩的 CPU 开销。 type GzipStatic struct { root string precompressedExtensions []string extensions []string enabled bool } // NewGzipStatic 创建预压缩文件处理器。 // // 参数: // - enabled: 是否启用预压缩支持 // - root: 静态文件根目录 // - extensions: 支持预压缩的文件扩展名,为空则使用默认值 func NewGzipStatic(enabled bool, root string, extensions []string) *GzipStatic { if len(extensions) == 0 { extensions = []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"} } return &GzipStatic{ enabled: enabled, root: root, extensions: extensions, precompressedExtensions: []string{".br", ".gz"}, } } // ServeFile 发送预压缩文件(如果存在)。 // // 检查是否存在对应的 .br 或 .gz 文件,按优先级(br > gzip)选择: // 1. 如果客户端支持 br 且 .br 文件存在,返回 .br 文件 // 2. 否则如果客户端支持 gzip 且 .gz 文件存在,返回 .gz 文件 // // 参数: // - ctx: FastHTTP 请求上下文 // - filePath: 请求的文件路径 // // 返回值: // - bool: true 表示已发送预压缩文件,false 表示未发送 func (g *GzipStatic) ServeFile(ctx *fasthttp.RequestCtx, filePath string) bool { if !g.enabled { return false } // 检查文件扩展名 if !g.matchExtension(filePath) { return false } // 安全检查:防止目录遍历 if strings.Contains(filePath, "..") { return false } // 获取 Accept-Encoding 头 acceptEncoding := ctx.Request.Header.Peek("Accept-Encoding") // 按优先级检查预压缩文件 for _, ext := range g.precompressedExtensions { // 检查客户端是否支持该编码 if !supportsEncoding(acceptEncoding, ext) { continue } // 构建预压缩文件路径 compressedPath := filePath + ext fullPath := filepath.Join(g.root, compressedPath) // 检查文件是否存在 if _, err := os.Stat(fullPath); err != nil { continue } // 设置 Content-Encoding 头 switch ext { case ".br": ctx.Response.Header.Set("Content-Encoding", "br") case ".gz": ctx.Response.Header.Set("Content-Encoding", "gzip") } ctx.Response.Header.Set("Vary", "Accept-Encoding") // 设置原始文件的 Content-Type // filePath 是原始文件路径 (如 "test.js"),直接使用即可 ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath)) fasthttp.ServeFile(ctx, fullPath) return true } return false } // matchExtension 检查文件扩展名是否匹配。 func (g *GzipStatic) matchExtension(filePath string) bool { ext := strings.ToLower(filepath.Ext(filePath)) for _, e := range g.extensions { if strings.ToLower(e) == ext { return true } } return false } // Enabled 返回是否启用预压缩。 func (g *GzipStatic) Enabled() bool { return g.enabled } // Extensions 返回支持的扩展名列表。 func (g *GzipStatic) Extensions() []string { return g.extensions } // supportsEncoding 检查客户端是否支持指定编码。 // // 简单检查,忽略 q-value,固定优先级由遍历顺序决定。 func supportsEncoding(acceptEncoding []byte, ext string) bool { if len(acceptEncoding) == 0 { return false } enc := strings.ToLower(string(acceptEncoding)) switch ext { case ".br": return strings.Contains(enc, "br") case ".gz": return strings.Contains(enc, "gzip") default: return false } }