缓存处理修补

This commit is contained in:
ryan
2026-06-11 19:07:22 +08:00
parent 5915b31519
commit 46760d4286
2 changed files with 113 additions and 11 deletions
+83 -11
View File
@@ -86,13 +86,83 @@ func getUploadRecordByID(c *gin.Context) (*model.Upload, error) {
return &upload, nil
}
// fileTypeCategory 定义文件的大类,便于未来扩展不同的处理方式
type fileTypeCategory string
const (
fileTypeImage fileTypeCategory = "image"
fileTypeVideo fileTypeCategory = "video"
fileTypeAudio fileTypeCategory = "audio"
fileTypeOther fileTypeCategory = "other"
)
// getFileTypeCategory 判断并返回文件的大类
func getFileTypeCategory(upload *model.Upload) fileTypeCategory {
mime := strings.ToLower(upload.MimeType)
ext := strings.ToLower(upload.Extension)
if strings.HasPrefix(mime, "image/") || isImageExtension(ext) {
return fileTypeImage
}
if strings.HasPrefix(mime, "video/") {
return fileTypeVideo
}
if strings.HasPrefix(mime, "audio/") {
return fileTypeAudio
}
return fileTypeOther
}
// ServeUpload 将已存在的文件内容读取并流式响应给客户端,支持本地和 S3/CDN 驱动,并可选支持 WebP 图片压缩与本地缓存。
func ServeUpload(c *gin.Context, upload *model.Upload) {
quality := normalizeImageQuality(c.Query("quality"))
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
// 设置通用的缓存控制响应头
setCacheHeaders(c, upload)
if quality == imageQualityOrigin || !isImage {
serveOriginal(c, upload)
category := getFileTypeCategory(upload)
quality := normalizeImageQuality(c.Query("quality"))
switch category {
case fileTypeImage:
// 如果是图片且不是原图质量,则提供压缩优化后的图片预览
if quality != imageQualityOrigin {
serveCompressedImage(c, upload, quality)
return
}
// 请求原图质量时,退化到默认提供原文件
fallthrough
default:
// 默认提供原文件,并执行协商缓存校验
serveOriginalWithConditionalCheck(c, upload)
}
}
func setCacheHeaders(c *gin.Context, upload *model.Upload) {
if isFilePublic(c.Request.Context(), upload.Type) {
c.Header("Cache-Control", "public, max-age=31536000")
} else {
c.Header("Cache-Control", "private, no-cache")
}
}
func serveOriginalWithConditionalCheck(c *gin.Context, upload *model.Upload) {
etag := fmt.Sprintf(`W/"%s"`, upload.Hash)
c.Header("ETag", etag)
if c.GetHeader("If-None-Match") == etag {
c.AbortWithStatus(http.StatusNotModified)
return
}
serveOriginal(c, upload)
}
func serveCompressedImage(c *gin.Context, upload *model.Upload, quality string) {
etag := fmt.Sprintf(`W/"%s-%s"`, upload.Hash, quality)
c.Header("ETag", etag)
if c.GetHeader("If-None-Match") == etag {
c.AbortWithStatus(http.StatusNotModified)
return
}
@@ -211,11 +281,11 @@ func getOriginalFileBytes(ctx context.Context, upload *model.Upload) ([]byte, er
return io.ReadAll(obj.Body)
}
// checkFileAccessPermission 校验文件是否可以被当前请求访问
func checkFileAccessPermission(c *gin.Context, uploadType string) error {
// isFilePublic 校验文件类型是否在公开访问白名单中
func isFilePublic(ctx context.Context, uploadType string) bool {
var sc model.SystemConfig
var whitelist []string
if err := sc.GetByKey(c.Request.Context(), model.ConfigKeyFileAccessWhitelist); err == nil && sc.Value != "" {
if err := sc.GetByKey(ctx, model.ConfigKeyFileAccessWhitelist); err == nil && sc.Value != "" {
if err := json.Unmarshal([]byte(sc.Value), &whitelist); err != nil {
// 降级使用逗号分隔解析
parts := strings.Split(sc.Value, ",")
@@ -231,15 +301,17 @@ func checkFileAccessPermission(c *gin.Context, uploadType string) error {
whitelist = []string{"avatar"}
}
inWhitelist := false
for _, w := range whitelist {
if strings.EqualFold(w, uploadType) {
inWhitelist = true
break
return true
}
}
return false
}
if !inWhitelist {
// checkFileAccessPermission 校验文件是否可以被当前请求访问
func checkFileAccessPermission(c *gin.Context, uploadType string) error {
if !isFilePublic(c.Request.Context(), uploadType) {
// 必须进行鉴权
if _, err := oauth.GetUserFromRequest(c); err != nil {
return err
+30
View File
@@ -320,6 +320,36 @@ func TestImageCompression(t *testing.T) {
}
})
t.Run("serve compressed WebP file and check cache headers and 304 Not Modified", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/f/3001?quality=medium", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
etag := w.Header().Get("ETag")
if etag == "" {
t.Error("expected ETag header, got empty")
}
cacheControl := w.Header().Get("Cache-Control")
if cacheControl != "public, max-age=31536000" {
t.Errorf("expected Cache-Control 'public, max-age=31536000', got %q", cacheControl)
}
// Perform conditional GET request
reqCond, _ := http.NewRequest("GET", "/f/3001?quality=medium", nil)
reqCond.Header.Set("If-None-Match", etag)
wCond := httptest.NewRecorder()
r.ServeHTTP(wCond, reqCond)
if wCond.Code != http.StatusNotModified {
t.Errorf("expected status 304, got %d", wCond.Code)
}
})
t.Run("serve original file with origin quality", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/f/3001?quality=origin", nil)
w := httptest.NewRecorder()