Files
OpenFlare/internal/infra/objectstore/storage.go
T
ryan 55db1c01ec 后台 goroutine panic 防护:新增 pkg/util.Go 共享助手(recover+调用点日志),全仓 22 个裸 go func() 站点统一收口
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":112,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 11:06:23 +08:00

219 lines
5.7 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package objectstore
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/util"
"gorm.io/gorm"
)
const (
defaultContentType = "application/octet-stream"
storageDirPerm = 0o750
storageFilePerm = 0o600
)
// Object describes a readable stored object.
type Object struct {
CachePath string
Body io.ReadCloser
ContentLength int64
ContentType string
}
// PutResult describes the result of a successful Put operation.
type PutResult struct {
Key string
Bucket string
}
// Backend defines storage operations used by the upload domain.
type Backend interface {
Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (PutResult, error)
Get(ctx context.Context, key string) (*Object, error)
Delete(ctx context.Context, key string) error
Test(ctx context.Context) error
}
var (
// IsEnabledFunc preserves the legacy S3 test hook while tests migrate to backend injection.
IsEnabledFunc = func() bool { return false }
mockBackend Backend
activeBackend Backend
activeDriver Driver
activeConfigJSON string
lastChecked time.Time
cacheMutex sync.RWMutex
)
// ConfigInvalidationChannel is the Redis pub/sub channel used to evict storage caches cluster-wide.
const ConfigInvalidationChannel = "storage:config_invalidation"
var pubSubOnce sync.Once
// ResetCache clears the local cache for storage configuration and client singletons.
func ResetCache() {
cacheMutex.Lock()
defer cacheMutex.Unlock()
activeBackend = nil
activeDriver = ""
activeConfigJSON = ""
lastChecked = time.Time{}
}
// PublishCacheInvalidation broadcasts cache eviction to all nodes in the cluster via Redis.
func PublishCacheInvalidation(ctx context.Context) {
if db.Redis != nil {
_ = db.Redis.Publish(ctx, ConfigInvalidationChannel, "reset").Err()
}
}
// startPubSubListener starts the background subscriber for cache invalidations.
func startPubSubListener() {
if db.Redis == nil {
return
}
util.Go(func() {
pubsub := db.Redis.Subscribe(context.Background(), ConfigInvalidationChannel)
defer func() {
_ = pubsub.Close()
}()
ch := pubsub.Channel()
for range ch {
ResetCache()
}
})
}
// Active returns the configured active driver and backend, using an in-memory cache with 5s TTL.
func Active(ctx context.Context) (Driver, Backend, error) {
if IsEnabledFunc() && mockBackend != nil {
return DriverS3, mockBackend, nil
}
pubSubOnce.Do(startPubSubListener)
cacheMutex.RLock()
isCacheValid := time.Since(lastChecked) < 5*time.Second && activeBackend != nil
if isCacheValid {
d, b := activeDriver, activeBackend
cacheMutex.RUnlock()
return d, b, nil
}
cacheMutex.RUnlock()
cacheMutex.Lock()
defer cacheMutex.Unlock()
// Double-check under write lock
if time.Since(lastChecked) < 5*time.Second && activeBackend != nil {
return activeDriver, activeBackend, nil
}
sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyStorageConfig)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil, err
}
lastChecked = time.Now()
// Reuse existing backend client singleton if configuration JSON matches
if sc.Value == activeConfigJSON && activeBackend != nil {
return activeDriver, activeBackend, nil
}
cfg := DefaultConfig()
if strings.TrimSpace(sc.Value) != "" {
if err := json.Unmarshal([]byte(sc.Value), &cfg); err != nil {
return "", nil, fmt.Errorf("parse storage config: %w", err)
}
}
backend, err := NewBackend(ctx, cfg, cfg.Driver)
if err != nil {
return "", nil, err
}
activeDriver = cfg.Driver
activeBackend = backend
activeConfigJSON = sc.Value
return activeDriver, activeBackend, nil
}
type functionBackend struct {
put func(context.Context, string, io.Reader, int64, string) error
get func(context.Context, string) (*Object, error)
delete func(context.Context, string) error
}
func (b *functionBackend) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (PutResult, error) {
if err := b.put(ctx, key, body, size, contentType); err != nil {
return PutResult{}, err
}
return PutResult{Key: key}, nil
}
func (b *functionBackend) Get(ctx context.Context, key string) (*Object, error) {
return b.get(ctx, key)
}
func (b *functionBackend) Delete(ctx context.Context, key string) error {
return b.delete(ctx, key)
}
func (b *functionBackend) Test(context.Context) error {
return nil
}
// MockStorage replaces object operations for package tests and returns a restore function.
func MockStorage(
put func(context.Context, string, io.Reader, int64, string) error,
get func(context.Context, string) (*Object, error),
deleteObject func(context.Context, string) error,
) func() {
previous := mockBackend
mockBackend = &functionBackend{put: put, get: get, delete: deleteObject}
return func() {
mockBackend = previous
}
}
// NewBackend constructs a concrete backend from configuration.
func NewBackend(ctx context.Context, cfg Config, driver Driver) (Backend, error) {
if driver == DriverS3 && mockBackend != nil {
return mockBackend, nil
}
switch driver {
case DriverLocal:
return newLocalBackend(cfg.Local)
case DriverS3:
return newS3Backend(ctx, cfg.S3)
case DriverR2:
return newR2Backend(ctx, cfg.R2)
case DriverMinIO:
return newS3Backend(ctx, cfg.MinIO)
case DriverOSS:
return newOSSBackend(cfg.OSS)
case DriverWebDAV:
return newWebDAVBackend(cfg.WebDAV)
default:
return nil, fmt.Errorf("unsupported storage driver %q", driver)
}
}