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 仅允许内部访问
|
||||
// 设置为 true 时,该位置仅允许内部重定向访问
|
||||
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 反向代理配置,支持负载均衡和健康检查。
|
||||
|
||||
@ -355,13 +355,17 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
||||
buf.WriteString(" try_files: [] # SPA 部署示例: [\"$uri\", \"$uri/\", \"/index.html\"]\n")
|
||||
buf.WriteString(" try_files_pass: 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(" # internal: false # 仅允许内部重定向访问\n")
|
||||
}
|
||||
buf.WriteString(" # 示例:使用 alias 替换路径\n")
|
||||
buf.WriteString(" # - path: \"/images/\"\n")
|
||||
buf.WriteString(" # alias: \"/var/www/files/\" # /images/logo.png → /var/www/files/logo.png\n")
|
||||
buf.WriteString(" # index: [\"index.html\"]\n")
|
||||
buf.WriteString(" # 示例:静态资源缓存配置\n")
|
||||
buf.WriteString(" # - path: \"/assets/\"\n")
|
||||
buf.WriteString(" # root: \"/var/www/assets\"\n")
|
||||
buf.WriteString(" # expires: \"30d\" # 缓存 30 天\n")
|
||||
buf.WriteString("\n")
|
||||
|
||||
// proxy 配置示例
|
||||
|
||||
@ -844,6 +844,10 @@ func convertStaticDirectives(directives []Directive, static *config.StaticConfig
|
||||
static.Index = append(static.Index, d.Args...)
|
||||
case "try_files":
|
||||
static.TryFiles = append(static.TryFiles, d.Args...)
|
||||
case "expires":
|
||||
if len(d.Args) > 0 {
|
||||
static.Expires = d.Args[0]
|
||||
}
|
||||
default:
|
||||
if msg, ok := unsupportedDirectives[d.Name]; ok {
|
||||
result.Warnings = append(result.Warnings, Warning{
|
||||
|
||||
@ -52,6 +52,7 @@ type StaticHandler struct {
|
||||
root string
|
||||
alias string
|
||||
pathPrefix string
|
||||
expires string // 缓存过期时间(nginx 兼容格式)
|
||||
// 切片字段
|
||||
index []string
|
||||
tryFiles []string
|
||||
@ -224,6 +225,17 @@ func (h *StaticHandler) SetInternal(enabled bool) {
|
||||
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。
|
||||
//
|
||||
// TTL 控制缓存条目的新鲜度验证间隔。
|
||||
@ -561,6 +573,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
||||
ctx.Response.Header.Set("ETag", etag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
h.setCacheHeaders(ctx)
|
||||
ctx.Response.SkipBody = true
|
||||
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("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
h.setCacheHeaders(ctx)
|
||||
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.Set("ETag", etag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
h.setCacheHeaders(ctx)
|
||||
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.Set("ETag", etag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
h.setCacheHeaders(ctx)
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
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.Set("ETag", etag)
|
||||
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 验证符号链接是否安全。
|
||||
|
||||
@ -1098,6 +1098,11 @@ func (s *Server) registerStaticHandlersWithLocationEngine(cfg *config.ServerConf
|
||||
// 设置 internal 限制
|
||||
staticHandler.SetInternal(static.Internal)
|
||||
|
||||
// 设置缓存过期时间
|
||||
if static.Expires != "" {
|
||||
staticHandler.SetExpires(static.Expires)
|
||||
}
|
||||
|
||||
// 根据 LocationType 注册路由
|
||||
locType := static.LocationType
|
||||
if locType == "" {
|
||||
@ -1464,6 +1469,11 @@ func (s *Server) registerStaticHandlers(router *handler.Router, cfg *config.Serv
|
||||
// 设置符号链接安全检查
|
||||
staticHandler.SetSymlinkCheck(static.SymlinkCheck)
|
||||
|
||||
// 设置缓存过期时间
|
||||
if static.Expires != "" {
|
||||
staticHandler.SetExpires(static.Expires)
|
||||
}
|
||||
|
||||
// 设置 try_files 配置
|
||||
if len(static.TryFiles) > 0 {
|
||||
// 注意:tryFilesPass 需要路由器支持,当前实现传入 nil
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user