Files
OpenFlare/backend/plugins/domain/upload/cache/access_cache.go
T

155 lines
4.7 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package cache provides in-process upload access-control caches.
package cache
import (
"Wavelet/pkg/logger"
"Wavelet/plugins/domain/upload/shared"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"gorm.io/gorm"
uploadstorage "Wavelet/plugins/domain/upload/storage"
)
const fileAccessInvalidationChannel = "upload:file_access_invalidation"
var (
fileAccessWhitelistMu sync.RWMutex
fileAccessWhitelistTypes map[string]struct{}
fileAccessWhitelistValid bool
fileAccessWhitelistCheckedAt time.Time
)
// ResetAccessCaches clears in-process upload access caches.
func ResetAccessCaches() {
uploadstorage.ResetMigrationAccessCache()
fileAccessWhitelistMu.Lock()
fileAccessWhitelistValid = false
fileAccessWhitelistTypes = nil
fileAccessWhitelistMu.Unlock()
}
// PublishAccessCacheInvalidation broadcasts upload access cache eviction to all nodes.
func PublishAccessCacheInvalidation(ctx context.Context) {
if cache := shared.GetCache(ctx); cache != nil {
_ = cache.Invalidate(ctx, fileAccessInvalidationChannel)
}
ResetAccessCaches()
}
// IsFilePublic reports whether uploadType is in the public access whitelist.
func IsFilePublic(ctx context.Context, uploadType string) bool {
whitelist := loadFileAccessWhitelist(ctx)
_, ok := whitelist[strings.ToLower(uploadType)]
return ok
}
func loadFileAccessWhitelist(ctx context.Context) map[string]struct{} {
fileAccessWhitelistMu.RLock()
if fileAccessWhitelistValid && time.Since(fileAccessWhitelistCheckedAt) < time.Duration(shared.AccessCacheTTL)*time.Second {
types := fileAccessWhitelistTypes
fileAccessWhitelistMu.RUnlock()
return types
}
fileAccessWhitelistMu.RUnlock()
fileAccessWhitelistMu.Lock()
defer fileAccessWhitelistMu.Unlock()
if fileAccessWhitelistValid && time.Since(fileAccessWhitelistCheckedAt) < time.Duration(shared.AccessCacheTTL)*time.Second {
return fileAccessWhitelistTypes
}
types, err := fetchFileAccessWhitelist(ctx)
if err != nil {
logger.ErrorF(ctx, "[Upload] 读取公开访问白名单失败: %v", err)
if fileAccessWhitelistTypes != nil {
// A previously loaded list is still the best answer; keep it until its
// TTL rather than narrowing access because one read failed.
fileAccessWhitelistValid = true
fileAccessWhitelistCheckedAt = time.Now()
return fileAccessWhitelistTypes
}
// Nothing cached yet: serve the restricted default but stay invalid so the
// next request retries instead of pinning it for the whole TTL.
return fallbackFileAccessWhitelist()
}
fileAccessWhitelistTypes = types
fileAccessWhitelistValid = true
fileAccessWhitelistCheckedAt = time.Now()
return types
}
// fallbackFileAccessWhitelist is the restricted default used when nothing better is known.
func fallbackFileAccessWhitelist() map[string]struct{} {
return map[string]struct{}{strings.ToLower(shared.DefaultPublicUploadType): {}}
}
func fetchFileAccessWhitelist(ctx context.Context) (map[string]struct{}, error) {
whitelist, err := parseFileAccessWhitelist(ctx)
if err != nil {
return nil, err
}
types := make(map[string]struct{}, len(whitelist))
for _, item := range whitelist {
types[strings.ToLower(item)] = struct{}{}
}
return types, nil
}
// parseFileAccessWhitelist reads the configured public access types.
//
// A missing row or an empty value is a real answer and yields the default; only a
// read that actually fails is reported as an error, so a database outage is no
// longer mistaken for "the admin never configured a whitelist".
func parseFileAccessWhitelist(ctx context.Context) ([]string, error) {
var sc struct{ Value string }
db := shared.GetDB(ctx)
if db == nil {
return nil, errors.New("database not available")
}
if err := db.Table("w_system_configs").Where("key = ?", "file_access_whitelist").First(&sc).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return []string{shared.DefaultPublicUploadType}, nil
}
return nil, fmt.Errorf("read file_access_whitelist: %w", err)
}
if sc.Value == "" {
return []string{shared.DefaultPublicUploadType}, nil
}
var whitelist []string
if err := json.Unmarshal([]byte(sc.Value), &whitelist); err == nil && len(whitelist) > 0 {
return whitelist, nil
}
whitelist = parseCommaSeparatedWhitelist(sc.Value)
if len(whitelist) == 0 {
return []string{shared.DefaultPublicUploadType}, nil
}
return whitelist, nil
}
func parseCommaSeparatedWhitelist(value string) []string {
parts := strings.Split(value, ",")
whitelist := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
whitelist = append(whitelist, part)
}
}
return whitelist
}