feat(static): 添加 expires 缓存控制支持
- StaticConfig 添加 Expires 字段,支持 nginx 兼容格式(30d, 1h, max, epoch) - StaticHandler 添加 SetExpires 方法和缓存响应头设置 - serveFile 自动设置 Cache-Control 和 Expires 响应头 - nginx 转换器正确转换 expires 指令 - --generate-config 输出包含 expires 文档和示例 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
02265331d4
commit
d191e1865d
@ -319,6 +319,12 @@ type StaticConfig struct {
|
|||||||
// Internal 仅允许内部访问
|
// Internal 仅允许内部访问
|
||||||
// 设置为 true 时,该位置仅允许内部重定向访问
|
// 设置为 true 时,该位置仅允许内部重定向访问
|
||||||
Internal bool `yaml:"internal"`
|
Internal bool `yaml:"internal"`
|
||||||
|
|
||||||
|
// Expires 缓存过期时间
|
||||||
|
// 支持 nginx 兼容格式:30d, 1h, 1m, max, epoch, off
|
||||||
|
// 设置 Cache-Control: max-age 和 Expires 响应头
|
||||||
|
// 示例:expires: 30d → Cache-Control: max-age=2592000
|
||||||
|
Expires string `yaml:"expires"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig 反向代理配置,支持负载均衡和健康检查。
|
// ProxyConfig 反向代理配置,支持负载均衡和健康检查。
|
||||||
|
|||||||
@ -355,13 +355,17 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
|||||||
buf.WriteString(" try_files: [] # SPA 部署示例: [\"$uri\", \"$uri/\", \"/index.html\"]\n")
|
buf.WriteString(" try_files: [] # SPA 部署示例: [\"$uri\", \"$uri/\", \"/index.html\"]\n")
|
||||||
buf.WriteString(" try_files_pass: false # 内部重定向是否触发中间件\n")
|
buf.WriteString(" try_files_pass: false # 内部重定向是否触发中间件\n")
|
||||||
buf.WriteString(" symlink_check: false # 是否检查符号链接安全(防止路径遍历攻击)\n")
|
buf.WriteString(" symlink_check: false # 是否检查符号链接安全(防止路径遍历攻击)\n")
|
||||||
|
buf.WriteString(" # expires: \"\" # 缓存过期时间(nginx 兼容格式:30d, 1h, max, epoch)\n")
|
||||||
|
buf.WriteString(" # 30d → Cache-Control: max-age=2592000\n")
|
||||||
|
buf.WriteString(" # max → Cache-Control: max-age=315360000, immutable\n")
|
||||||
|
buf.WriteString(" # epoch → Cache-Control: no-cache\n")
|
||||||
buf.WriteString(" # location_type: \"\" # 位置匹配类型(有效值: exact, prefix, regex, regex_caseless, prefix_priority, named)\n")
|
buf.WriteString(" # location_type: \"\" # 位置匹配类型(有效值: exact, prefix, regex, regex_caseless, prefix_priority, named)\n")
|
||||||
buf.WriteString(" # internal: false # 仅允许内部重定向访问\n")
|
buf.WriteString(" # internal: false # 仅允许内部重定向访问\n")
|
||||||
}
|
}
|
||||||
buf.WriteString(" # 示例:使用 alias 替换路径\n")
|
buf.WriteString(" # 示例:静态资源缓存配置\n")
|
||||||
buf.WriteString(" # - path: \"/images/\"\n")
|
buf.WriteString(" # - path: \"/assets/\"\n")
|
||||||
buf.WriteString(" # alias: \"/var/www/files/\" # /images/logo.png → /var/www/files/logo.png\n")
|
buf.WriteString(" # root: \"/var/www/assets\"\n")
|
||||||
buf.WriteString(" # index: [\"index.html\"]\n")
|
buf.WriteString(" # expires: \"30d\" # 缓存 30 天\n")
|
||||||
buf.WriteString("\n")
|
buf.WriteString("\n")
|
||||||
|
|
||||||
// proxy 配置示例
|
// proxy 配置示例
|
||||||
|
|||||||
@ -844,6 +844,10 @@ func convertStaticDirectives(directives []Directive, static *config.StaticConfig
|
|||||||
static.Index = append(static.Index, d.Args...)
|
static.Index = append(static.Index, d.Args...)
|
||||||
case "try_files":
|
case "try_files":
|
||||||
static.TryFiles = append(static.TryFiles, d.Args...)
|
static.TryFiles = append(static.TryFiles, d.Args...)
|
||||||
|
case "expires":
|
||||||
|
if len(d.Args) > 0 {
|
||||||
|
static.Expires = d.Args[0]
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if msg, ok := unsupportedDirectives[d.Name]; ok {
|
if msg, ok := unsupportedDirectives[d.Name]; ok {
|
||||||
result.Warnings = append(result.Warnings, Warning{
|
result.Warnings = append(result.Warnings, Warning{
|
||||||
|
|||||||
@ -52,6 +52,7 @@ type StaticHandler struct {
|
|||||||
root string
|
root string
|
||||||
alias string
|
alias string
|
||||||
pathPrefix string
|
pathPrefix string
|
||||||
|
expires string // 缓存过期时间(nginx 兼容格式)
|
||||||
// 切片字段
|
// 切片字段
|
||||||
index []string
|
index []string
|
||||||
tryFiles []string
|
tryFiles []string
|
||||||
@ -224,6 +225,17 @@ func (h *StaticHandler) SetInternal(enabled bool) {
|
|||||||
h.internal = enabled
|
h.internal = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpires 设置缓存过期时间。
|
||||||
|
//
|
||||||
|
// 支持 nginx 兼容格式:30d, 1h, 1m, max, epoch, off
|
||||||
|
// 设置后会在响应中添加 Cache-Control 和 Expires 头。
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - expires: 过期时间字符串
|
||||||
|
func (h *StaticHandler) SetExpires(expires string) {
|
||||||
|
h.expires = expires
|
||||||
|
}
|
||||||
|
|
||||||
// SetCacheTTL 设置缓存新鲜度 TTL。
|
// SetCacheTTL 设置缓存新鲜度 TTL。
|
||||||
//
|
//
|
||||||
// TTL 控制缓存条目的新鲜度验证间隔。
|
// TTL 控制缓存条目的新鲜度验证间隔。
|
||||||
@ -561,6 +573,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
|
|||||||
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
||||||
ctx.Response.Header.Set("ETag", etag)
|
ctx.Response.Header.Set("ETag", etag)
|
||||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||||
|
h.setCacheHeaders(ctx)
|
||||||
ctx.Response.SkipBody = true
|
ctx.Response.SkipBody = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -572,6 +585,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
|
|||||||
// 预压缩文件已发送,补充验证头
|
// 预压缩文件已发送,补充验证头
|
||||||
ctx.Response.Header.Set("ETag", etag)
|
ctx.Response.Header.Set("ETag", etag)
|
||||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||||
|
h.setCacheHeaders(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -586,6 +600,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
|
|||||||
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
|
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
|
||||||
ctx.Response.Header.Set("ETag", etag)
|
ctx.Response.Header.Set("ETag", etag)
|
||||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||||
|
h.setCacheHeaders(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 文件已修改,删除旧缓存
|
// 文件已修改,删除旧缓存
|
||||||
@ -603,6 +618,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
|
|||||||
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
|
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
|
||||||
ctx.Response.Header.Set("ETag", etag)
|
ctx.Response.Header.Set("ETag", etag)
|
||||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||||
|
h.setCacheHeaders(ctx)
|
||||||
|
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -629,6 +645,69 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
|
|||||||
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
|
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
|
||||||
ctx.Response.Header.Set("ETag", etag)
|
ctx.Response.Header.Set("ETag", etag)
|
||||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||||
|
h.setCacheHeaders(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCacheHeaders 设置缓存控制响应头。
|
||||||
|
func (h *StaticHandler) setCacheHeaders(ctx *fasthttp.RequestCtx) {
|
||||||
|
if h.expires == "" || h.expires == "off" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.expires == "epoch" {
|
||||||
|
ctx.Response.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
ctx.Response.Header.Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.expires == "max" {
|
||||||
|
ctx.Response.Header.Set("Cache-Control", "public, max-age=315360000, immutable")
|
||||||
|
ctx.Response.Header.Set("Expires", time.Now().Add(315360000*time.Second).UTC().Format(httpTimeFormat))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxAge := parseExpires(h.expires)
|
||||||
|
if maxAge > 0 {
|
||||||
|
ctx.Response.Header.Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
|
ctx.Response.Header.Set("Expires", time.Now().Add(time.Duration(maxAge)*time.Second).UTC().Format(httpTimeFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExpires 解析 nginx 兼容的过期时间格式。
|
||||||
|
// 支持格式:30d, 1h, 1m, 1s, 30d1h 等
|
||||||
|
// 返回秒数。
|
||||||
|
func parseExpires(expires string) int64 {
|
||||||
|
if expires == "" || expires == "off" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if expires == "max" {
|
||||||
|
return 315360000
|
||||||
|
}
|
||||||
|
if expires == "epoch" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
var num int64
|
||||||
|
for _, ch := range expires {
|
||||||
|
switch {
|
||||||
|
case ch >= '0' && ch <= '9':
|
||||||
|
num = num*10 + int64(ch-'0')
|
||||||
|
case ch == 'd':
|
||||||
|
total += num * 86400
|
||||||
|
num = 0
|
||||||
|
case ch == 'h':
|
||||||
|
total += num * 3600
|
||||||
|
num = 0
|
||||||
|
case ch == 'm':
|
||||||
|
total += num * 60
|
||||||
|
num = 0
|
||||||
|
case ch == 's':
|
||||||
|
total += num
|
||||||
|
num = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateSymlink 验证符号链接是否安全。
|
// validateSymlink 验证符号链接是否安全。
|
||||||
|
|||||||
@ -1098,6 +1098,11 @@ func (s *Server) registerStaticHandlersWithLocationEngine(cfg *config.ServerConf
|
|||||||
// 设置 internal 限制
|
// 设置 internal 限制
|
||||||
staticHandler.SetInternal(static.Internal)
|
staticHandler.SetInternal(static.Internal)
|
||||||
|
|
||||||
|
// 设置缓存过期时间
|
||||||
|
if static.Expires != "" {
|
||||||
|
staticHandler.SetExpires(static.Expires)
|
||||||
|
}
|
||||||
|
|
||||||
// 根据 LocationType 注册路由
|
// 根据 LocationType 注册路由
|
||||||
locType := static.LocationType
|
locType := static.LocationType
|
||||||
if locType == "" {
|
if locType == "" {
|
||||||
@ -1464,6 +1469,11 @@ func (s *Server) registerStaticHandlers(router *handler.Router, cfg *config.Serv
|
|||||||
// 设置符号链接安全检查
|
// 设置符号链接安全检查
|
||||||
staticHandler.SetSymlinkCheck(static.SymlinkCheck)
|
staticHandler.SetSymlinkCheck(static.SymlinkCheck)
|
||||||
|
|
||||||
|
// 设置缓存过期时间
|
||||||
|
if static.Expires != "" {
|
||||||
|
staticHandler.SetExpires(static.Expires)
|
||||||
|
}
|
||||||
|
|
||||||
// 设置 try_files 配置
|
// 设置 try_files 配置
|
||||||
if len(static.TryFiles) > 0 {
|
if len(static.TryFiles) > 0 {
|
||||||
// 注意:tryFilesPass 需要路由器支持,当前实现传入 nil
|
// 注意:tryFilesPass 需要路由器支持,当前实现传入 nil
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user