图片缓存不过期

This commit is contained in:
ryan
2026-06-11 15:57:31 +08:00
parent 1eef5336e3
commit f14875a8de
4 changed files with 77 additions and 8 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
## 缓存与文件能力
- `internal/diskcache`:通过 `diskcache.GetGlobalCache()` 提供字节缓存读写、TTL、最大空间限制、LRU 淘汰、清空、状态统计和配置热更新。
- `internal/diskcache`:通过 `diskcache.GetGlobalCache()` 提供字节缓存读写、TTL、最大空间限制、LRU 淘汰、清空、状态统计和配置热更新。写入时使用 `DefaultExpiration`(全局默认 TTL)、正数 `time.Duration`(业务 TTL)或 `NoExpiration`(值为 `-1`,永不过期);永久缓存仍受空间限制和 LRU 淘汰。
- `internal/storage`:提供 S3 兼容对象上传、读取、删除、CDN/代理读取及远端文件本地缓存。
- `internal/apps/upload`:提供上传记录、文件访问控制、本地/S3 文件响应、下载及图片 WebP 压缩;业务应复用这些入口,不直接操作底层文件。
+1 -1
View File
@@ -121,7 +121,7 @@ func ServeUpload(c *gin.Context, upload *model.Upload) {
return
}
if err := cache.Set(cacheKey, webpBytes, 0); err != nil {
if err := cache.Set(cacheKey, webpBytes, diskcache.NoExpiration); err != nil {
logger.WarnF(c.Request.Context(), "failed to cache compressed image: %v", err)
}
+9 -3
View File
@@ -30,6 +30,11 @@ const (
defaultMaxSizeMB = 100
defaultTTLMinutes = 60
defaultCleanupInterval = 10
// DefaultExpiration applies the cache-wide default TTL.
DefaultExpiration time.Duration = 0
// NoExpiration stores the item without a TTL. Size limits and LRU eviction still apply.
NoExpiration time.Duration = -1
)
var (
@@ -103,13 +108,14 @@ func New(basePath string) *DiskCache {
return c
}
// Set stores a key-value pair in the cache with a specific TTL.
// If ttl <= 0, the default cache TTL is used.
// Set stores a key-value pair in the cache.
// Use DefaultExpiration for the configured default TTL, NoExpiration for no
// TTL, or a positive duration for a business-specific TTL.
func (c *DiskCache) Set(key string, value []byte, ttl time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
if ttl <= 0 {
if ttl == DefaultExpiration {
ttl = c.defaultTTL
}
+66 -3
View File
@@ -95,6 +95,69 @@ func TestDiskCacheTTL(t *testing.T) {
}
}
func TestDiskCacheExpirationPolicies(t *testing.T) {
testDir := "uploads/test_diskcache_expiration_policies"
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir)
defer func() { _ = c.Clear() }()
c.defaultTTL = 50 * time.Millisecond
if err := c.Set("default", []byte("default"), DefaultExpiration); err != nil {
t.Fatalf("Set(default, DefaultExpiration) returned error: %v", err)
}
if err := c.Set("custom", []byte("custom"), 100*time.Millisecond); err != nil {
t.Fatalf("Set(custom, 100ms) returned error: %v", err)
}
if err := c.Set("permanent", []byte("permanent"), NoExpiration); err != nil {
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
}
time.Sleep(75 * time.Millisecond)
if _, err := c.Get("default"); err != ErrCacheMiss {
t.Errorf("Get(default) error = %v, want ErrCacheMiss", err)
}
if _, err := c.Get("custom"); err != nil {
t.Errorf("Get(custom) returned error before custom TTL elapsed: %v", err)
}
if _, err := c.Get("permanent"); err != nil {
t.Errorf("Get(permanent) returned error: %v", err)
}
time.Sleep(50 * time.Millisecond)
if _, err := c.Get("custom"); err != ErrCacheMiss {
t.Errorf("Get(custom) error = %v, want ErrCacheMiss", err)
}
if _, err := c.Get("permanent"); err != nil {
t.Errorf("Get(permanent) returned error after other entries expired: %v", err)
}
}
func TestDiskCacheNoExpirationSurvivesReload(t *testing.T) {
testDir := "uploads/test_diskcache_no_expiration_reload"
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir)
if err := c.Set("permanent", []byte("value"), NoExpiration); err != nil {
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
}
reloaded := New(testDir)
defer func() { _ = reloaded.Clear() }()
got, err := reloaded.Get("permanent")
if err != nil {
t.Fatalf("reloaded Get(permanent) returned error: %v", err)
}
if !bytes.Equal(got, []byte("value")) {
t.Errorf("reloaded Get(permanent) = %q, want %q", got, "value")
}
}
func TestDiskCacheLRUEviction(t *testing.T) {
testDir := "uploads/test_diskcache_lru"
defer func() { _ = os.RemoveAll(testDir) }()
@@ -109,13 +172,13 @@ func TestDiskCacheLRUEviction(t *testing.T) {
c.lruEnabled = true
// Write item 1: 8 + 2 = 10 bytes
err := c.Set("k1", []byte("v1"), 0)
err := c.Set("k1", []byte("v1"), DefaultExpiration)
if err != nil {
t.Fatalf("failed to set k1: %v", err)
}
// Write item 2: 8 + 2 = 10 bytes
err = c.Set("k2", []byte("v2"), 0)
err = c.Set("k2", []byte("v2"), DefaultExpiration)
if err != nil {
t.Fatalf("failed to set k2: %v", err)
}
@@ -133,7 +196,7 @@ func TestDiskCacheLRUEviction(t *testing.T) {
// wait, let's access k1 again to make it the most recently used, so k2 becomes oldest!
_, _ = c.Get("k1") // k1 is now MRU, k2 is LRU
err = c.Set("k3", []byte("v3"), 0)
err = c.Set("k3", []byte("v3"), DefaultExpiration)
if err != nil {
t.Fatalf("failed to set k3: %v", err)
}