mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
图片压缩调用缓存
This commit is contained in:
@@ -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 压缩;业务应复用这些入口,不直接操作底层文件。
|
||||
|
||||
## 常用命令
|
||||
|
||||
|
||||
@@ -8,6 +8,4 @@ const (
|
||||
detectContentBytes = 512 // http.DetectContentType 需要的最小字节数
|
||||
uploadDirPerm = 0755 // 上传目录权限
|
||||
uploadFilePerm = 0644 // 上传文件权限
|
||||
cacheDirPerm = 0750 // 缓存目录权限 (gosec)
|
||||
cacheFilePerm = 0600 // 缓存文件权限 (gosec)
|
||||
)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user