mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 07:06:36 +08:00
9a18bea324
Move storage backend configuration from startup YAML to system_config-backed runtime configuration. Add local, S3-compatible, R2, MinIO, OSS, and WebDAV backend support. Add a storage migration async task using the existing task dispatch framework. Migration target config is carried in task payload, and maintenance mode is derived from task execution state. Split upload file management and storage operations, add the admin storage configuration tab, and update migrations and Swagger docs.
235 lines
6.9 KiB
Go
235 lines
6.9 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package storage provides dynamically configured file storage backends.
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/db"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Driver identifies a supported storage backend.
|
|
type Driver string
|
|
|
|
const (
|
|
// DriverLocal stores files on the local filesystem.
|
|
DriverLocal Driver = "local"
|
|
// DriverS3 stores files in an S3-compatible object store.
|
|
DriverS3 Driver = "s3"
|
|
// DriverR2 stores files in Cloudflare R2.
|
|
DriverR2 Driver = "r2"
|
|
// DriverMinIO stores files in MinIO.
|
|
DriverMinIO Driver = "minio"
|
|
// DriverOSS stores files in Aliyun OSS.
|
|
DriverOSS Driver = "oss"
|
|
// DriverWebDAV stores files through WebDAV.
|
|
DriverWebDAV Driver = "webdav"
|
|
|
|
// ConfigMask replaces secrets returned to the frontend.
|
|
ConfigMask = "******"
|
|
)
|
|
|
|
// LocalConfig configures local filesystem storage.
|
|
type LocalConfig struct {
|
|
Root string `json:"root"`
|
|
}
|
|
|
|
// ObjectConfig configures S3-compatible or OSS object storage.
|
|
type ObjectConfig struct {
|
|
Endpoint string `json:"endpoint"`
|
|
Region string `json:"region"`
|
|
Bucket string `json:"bucket"`
|
|
AccessKeyID string `json:"access_key_id"`
|
|
SecretAccessKey string `json:"secret_access_key"`
|
|
AccountID string `json:"account_id,omitempty"`
|
|
PathStyle bool `json:"path_style"`
|
|
KeyPrefix string `json:"key_prefix"`
|
|
CDNURL string `json:"cdn_url"`
|
|
}
|
|
|
|
// WebDAVConfig configures WebDAV storage.
|
|
type WebDAVConfig struct {
|
|
Endpoint string `json:"endpoint"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
BasePath string `json:"base_path"`
|
|
}
|
|
|
|
// Config contains all storage backends and the currently active driver.
|
|
type Config struct {
|
|
Driver Driver `json:"driver"`
|
|
Local LocalConfig `json:"local"`
|
|
S3 ObjectConfig `json:"s3"`
|
|
R2 ObjectConfig `json:"r2"`
|
|
MinIO ObjectConfig `json:"minio"`
|
|
OSS ObjectConfig `json:"oss"`
|
|
WebDAV WebDAVConfig `json:"webdav"`
|
|
}
|
|
|
|
// DefaultConfig returns the local-storage default configuration.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
Driver: DriverLocal,
|
|
Local: LocalConfig{Root: "."},
|
|
S3: ObjectConfig{Region: "us-east-1"},
|
|
R2: ObjectConfig{Region: "auto"},
|
|
MinIO: ObjectConfig{Region: "us-east-1", PathStyle: true},
|
|
}
|
|
}
|
|
|
|
// LoadConfig loads the active storage configuration.
|
|
func LoadConfig(ctx context.Context) (Config, error) {
|
|
return loadConfigByKey(ctx, model.ConfigKeyStorageConfig, DefaultConfig())
|
|
}
|
|
|
|
func loadConfigByKey(ctx context.Context, key string, fallback Config) (Config, error) {
|
|
var sc model.SystemConfig
|
|
if err := sc.GetByKey(ctx, key); err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fallback, nil
|
|
}
|
|
return Config{}, err
|
|
}
|
|
if strings.TrimSpace(sc.Value) == "" {
|
|
return fallback, nil
|
|
}
|
|
if err := json.Unmarshal([]byte(sc.Value), &fallback); err != nil {
|
|
return Config{}, fmt.Errorf("parse %s: %w", key, err)
|
|
}
|
|
return fallback, nil
|
|
}
|
|
|
|
// ValidateConfig validates the selected backend configuration.
|
|
func ValidateConfig(cfg Config) error {
|
|
switch cfg.Driver {
|
|
case DriverLocal:
|
|
if strings.TrimSpace(cfg.Local.Root) == "" {
|
|
return errors.New("local root is required")
|
|
}
|
|
case DriverS3:
|
|
return validateObjectConfig(cfg.S3, false)
|
|
case DriverR2:
|
|
if strings.TrimSpace(cfg.R2.AccountID) == "" && strings.TrimSpace(cfg.R2.Endpoint) == "" {
|
|
return errors.New("R2 account ID or endpoint is required")
|
|
}
|
|
return validateObjectConfig(cfg.R2, false)
|
|
case DriverMinIO:
|
|
if strings.TrimSpace(cfg.MinIO.Endpoint) == "" {
|
|
return errors.New("MinIO endpoint is required")
|
|
}
|
|
return validateObjectConfig(cfg.MinIO, true)
|
|
case DriverOSS:
|
|
if strings.TrimSpace(cfg.OSS.Endpoint) == "" {
|
|
return errors.New("OSS endpoint is required")
|
|
}
|
|
return validateObjectConfig(cfg.OSS, true)
|
|
case DriverWebDAV:
|
|
if strings.TrimSpace(cfg.WebDAV.Endpoint) == "" {
|
|
return errors.New("WebDAV endpoint is required")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported storage driver %q", cfg.Driver)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateObjectConfig(cfg ObjectConfig, endpointRequired bool) error {
|
|
if endpointRequired && strings.TrimSpace(cfg.Endpoint) == "" {
|
|
return errors.New("endpoint is required")
|
|
}
|
|
if strings.TrimSpace(cfg.Region) == "" {
|
|
return errors.New("region is required")
|
|
}
|
|
if strings.TrimSpace(cfg.Bucket) == "" {
|
|
return errors.New("bucket is required")
|
|
}
|
|
if strings.TrimSpace(cfg.AccessKeyID) == "" || strings.TrimSpace(cfg.SecretAccessKey) == "" {
|
|
return errors.New("access key ID and secret access key are required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SaveActiveConfig persists the active storage configuration.
|
|
func SaveActiveConfig(ctx context.Context, cfg Config) error {
|
|
return saveSystemConfig(ctx, model.ConfigKeyStorageConfig, cfg, "文件存储驱动及连接配置(JSON)")
|
|
}
|
|
|
|
func saveSystemConfig(ctx context.Context, key string, value any, description string) error {
|
|
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return upsertSystemConfig(ctx, tx, key, value, description)
|
|
})
|
|
}
|
|
|
|
func upsertSystemConfig(ctx context.Context, tx *gorm.DB, key string, value any, description string) error {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal %s: %w", key, err)
|
|
}
|
|
sc := model.SystemConfig{
|
|
Key: key,
|
|
Value: string(data),
|
|
Type: "system",
|
|
Visibility: model.ConfigVisibilityHidden,
|
|
Description: description,
|
|
}
|
|
if err := tx.Where("key = ?", key).
|
|
Assign(map[string]any{"value": sc.Value, "description": description, "visibility": model.ConfigVisibilityHidden}).
|
|
FirstOrCreate(&sc).Error; err != nil {
|
|
return err
|
|
}
|
|
if db.Redis != nil {
|
|
if err := db.HSetJSON(ctx, model.SystemConfigRedisHashKey, key, &sc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MergeMaskedSecrets restores unchanged secrets from the current configuration.
|
|
func MergeMaskedSecrets(next, current Config) Config {
|
|
mergeObjectSecret := func(dst *ObjectConfig, src ObjectConfig) {
|
|
if dst.AccessKeyID == ConfigMask {
|
|
dst.AccessKeyID = src.AccessKeyID
|
|
}
|
|
if dst.SecretAccessKey == ConfigMask {
|
|
dst.SecretAccessKey = src.SecretAccessKey
|
|
}
|
|
}
|
|
mergeObjectSecret(&next.S3, current.S3)
|
|
mergeObjectSecret(&next.R2, current.R2)
|
|
mergeObjectSecret(&next.MinIO, current.MinIO)
|
|
mergeObjectSecret(&next.OSS, current.OSS)
|
|
if next.WebDAV.Password == ConfigMask {
|
|
next.WebDAV.Password = current.WebDAV.Password
|
|
}
|
|
return next
|
|
}
|
|
|
|
// MaskSecrets replaces stored credentials with placeholders for API responses.
|
|
func MaskSecrets(cfg Config) Config {
|
|
maskObject := func(value *ObjectConfig) {
|
|
if value.AccessKeyID != "" {
|
|
value.AccessKeyID = ConfigMask
|
|
}
|
|
if value.SecretAccessKey != "" {
|
|
value.SecretAccessKey = ConfigMask
|
|
}
|
|
}
|
|
maskObject(&cfg.S3)
|
|
maskObject(&cfg.R2)
|
|
maskObject(&cfg.MinIO)
|
|
maskObject(&cfg.OSS)
|
|
if cfg.WebDAV.Password != "" {
|
|
cfg.WebDAV.Password = ConfigMask
|
|
}
|
|
return cfg
|
|
}
|