图片压缩调用缓存

This commit is contained in:
ryan
2026-06-11 15:23:15 +08:00
parent 0221d4de14
commit ad97ca7df1
4 changed files with 57 additions and 31 deletions
+7
View File
@@ -18,6 +18,13 @@
- 所有 HTTP 路由仅在 `internal/router/router.go` 中注册。
- 当 API Handler 发生变化时,更新 Swagger 文档(运行 `make swagger`)。
- 在提交更改前运行 `make code-check`。
- 需要缓存或文件管理能力时,必须复用现有平台实现,禁止在业务包中自行创建缓存目录、直接管理缓存文件或重复封装存储后端。
## 缓存与文件能力
- `internal/diskcache`:通过 `diskcache.GetGlobalCache()` 提供字节缓存读写、TTL、最大空间限制、LRU 淘汰、清空、状态统计和配置热更新。
- `internal/storage`:提供 S3 兼容对象上传、读取、删除、CDN/代理读取及远端文件本地缓存。
- `internal/apps/upload`:提供上传记录、文件访问控制、本地/S3 文件响应、下载及图片 WebP 压缩;业务应复用这些入口,不直接操作底层文件。
## 常用命令
-2
View File
@@ -8,6 +8,4 @@ const (
detectContentBytes = 512 // http.DetectContentType 需要的最小字节数
uploadDirPerm = 0755 // 上传目录权限
uploadFilePerm = 0644 // 上传文件权限
cacheDirPerm = 0750 // 缓存目录权限 (gosec)
cacheFilePerm = 0600 // 缓存文件权限 (gosec)
)
+20 -14
View File
@@ -13,13 +13,13 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/common"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/diskcache"
"github.com/Rain-kl/Wavelet/internal/logger"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/storage"
@@ -103,14 +103,13 @@ func ServeUpload(c *gin.Context, upload *model.Upload) {
level = "high"
}
// Local cache path for the compressed webp image
cachePath := filepath.Join("uploads", "cache", fmt.Sprintf("compressed_%d_%s.webp", upload.ID, level))
// Check if the compressed file already exists in cache
if _, err := os.Stat(cachePath); err == nil {
c.Header("Content-Type", "image/webp")
c.File(cachePath)
cache := diskcache.GetGlobalCache()
cacheKey := imageCompressionCacheKey(upload, level)
if webpBytes, err := cache.Get(cacheKey); err == nil {
c.Data(http.StatusOK, "image/webp", webpBytes)
return
} else if !errors.Is(err, diskcache.ErrCacheMiss) {
logger.WarnF(c.Request.Context(), "failed to read compressed image cache: %v", err)
}
// Cache miss: retrieve original file content
@@ -129,18 +128,25 @@ func ServeUpload(c *gin.Context, upload *model.Upload) {
return
}
// Ensure cache directory exists and write cached file
if err := os.MkdirAll(filepath.Dir(cachePath), cacheDirPerm); err != nil {
logger.ErrorF(c.Request.Context(), "failed to create cache directory: %v", err)
} else if err := os.WriteFile(cachePath, webpBytes, cacheFilePerm); err != nil {
logger.ErrorF(c.Request.Context(), "failed to write compressed cache file: %v", err)
if err := cache.Set(cacheKey, webpBytes, 0); err != nil {
logger.WarnF(c.Request.Context(), "failed to cache compressed image: %v", err)
}
// Serve compressed WebP
c.Header("Content-Type", "image/webp")
c.Data(http.StatusOK, "image/webp", webpBytes)
}
func imageCompressionCacheKey(upload *model.Upload, level string) string {
return fmt.Sprintf(
"upload_webp_v1_%d_%d_%d_%s_%s",
upload.ID,
upload.UpdatedAt.UnixNano(),
upload.FileSize,
upload.Hash,
level,
)
}
// serveOriginal 原始文件的流式响应逻辑
func serveOriginal(c *gin.Context, upload *model.Upload) {
if upload.StorageDriver == "local" || (upload.StorageDriver == "" && !storage.IsEnabled()) {
+30 -15
View File
@@ -13,10 +13,10 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/Rain-kl/Wavelet/internal/common"
"github.com/Rain-kl/Wavelet/internal/diskcache"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/gin-contrib/sessions"
@@ -205,10 +205,20 @@ func TestImageCompression(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
cache := diskcache.GetGlobalCache()
if err := cache.Clear(); err != nil {
t.Fatalf("failed to clear disk cache before test: %v", err)
}
// Ensure uploads dir is cleaned up
defer func() {
_ = os.RemoveAll("uploads")
}()
defer func() {
if err := cache.Clear(); err != nil {
t.Errorf("failed to clear disk cache after test: %v", err)
}
}()
// Create test user
user := model.User{
@@ -282,25 +292,31 @@ func TestImageCompression(t *testing.T) {
t.Errorf("expected Content-Type image/webp, got %s", w.Header().Get("Content-Type"))
}
// Check if local cache file was created
cachePath := filepath.Join("uploads", "cache", "compressed_3001_medium.webp")
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
t.Errorf("expected cached webp file to be created at %s, but it doesn't exist", cachePath)
cacheKey := imageCompressionCacheKey(&uploadRecord, "medium")
cachedBytes, err := cache.Get(cacheKey)
if err != nil {
t.Fatalf("disk cache Get(%q) returned error: %v", cacheKey, err)
}
if !bytes.Equal(cachedBytes, w.Body.Bytes()) {
t.Errorf("cached compressed image differs from response")
}
// Subsequent request should hit the cache (modify the cached file to verify)
testBytes := []byte("cached webp content")
if err := os.WriteFile(cachePath, testBytes, 0644); err != nil {
t.Fatalf("failed to write test bytes to cache: %v", err)
if err := os.Remove(filePath); err != nil {
t.Fatalf("failed to remove source image before cache-hit request: %v", err)
}
t.Cleanup(func() {
if err := os.WriteFile(filePath, pngBuf.Bytes(), 0644); err != nil {
t.Errorf("failed to restore source image: %v", err)
}
})
w2 := httptest.NewRecorder()
r.ServeHTTP(w2, req)
if w2.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w2.Code)
}
if string(w2.Body.Bytes()) != "cached webp content" {
t.Errorf("expected cached content, got %s", string(w2.Body.Bytes()))
if !bytes.Equal(w2.Body.Bytes(), cachedBytes) {
t.Errorf("cache-hit response differs from cached compressed image")
}
})
@@ -312,10 +328,9 @@ func TestImageCompression(t *testing.T) {
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
// Check if local cache file with "high" was created
cachePath := filepath.Join("uploads", "cache", "compressed_3001_high.webp")
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
t.Errorf("expected cached webp file to be created at %s for default level", cachePath)
cacheKey := imageCompressionCacheKey(&uploadRecord, "high")
if _, err := cache.Get(cacheKey); err != nil {
t.Errorf("disk cache Get(%q) returned error: %v", cacheKey, err)
}
})
}