feat(admin): implement provider-specific storage configuration pages
Some checks failed
Build and Publish Docker Image / deploy (push) Has been cancelled
Some checks failed
Build and Publish Docker Image / deploy (push) Has been cancelled
Refactor the admin storage backend creation and editing flows to use provider-specific pages (e.g., `/admin/storage/new/sftp`) instead of a single generic form. This ensures only relevant fields are rendered for each storage provider (such as SFTP, S3, or WebDAV). Additionally: - Prevent mutation of the storage provider type during backend edits. - Add comprehensive unit tests for provider-specific rendering, edit validation, and CSRF/admin route protection.
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var storageBackendsBucket = []byte("storage_backends")
|
||||
var storageBackendTestStatusBucket = []byte("storage_backend_test_status")
|
||||
|
||||
const (
|
||||
StorageBackendLocal = "local"
|
||||
@@ -81,10 +82,12 @@ type StorageBackendConfig struct {
|
||||
}
|
||||
|
||||
type StorageBackendView struct {
|
||||
Config StorageBackendConfig
|
||||
UsageBytes int64
|
||||
UsageLabel string
|
||||
InUse bool
|
||||
Config StorageBackendConfig
|
||||
UsageBytes int64
|
||||
UsageLabel string
|
||||
InUse bool
|
||||
SpeedTests []StorageSpeedTest
|
||||
CanSpeedTest bool
|
||||
}
|
||||
|
||||
type StorageService struct {
|
||||
@@ -99,7 +102,13 @@ func NewStorageService(db *bbolt.DB, dataDir string) (*StorageService, error) {
|
||||
}
|
||||
service := &StorageService{db: db, localFilesDir: filesDir}
|
||||
err := db.Update(func(tx *bbolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists(storageBackendsBucket)
|
||||
if _, err := tx.CreateBucketIfNotExists(storageBackendsBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.CreateBucketIfNotExists(storageBackendTestStatusBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.CreateBucketIfNotExists(storageSpeedTestsBucket)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -126,7 +135,9 @@ func (s *StorageService) Backend(id string) (StorageBackend, error) {
|
||||
func (s *StorageService) BackendConfig(id string) (StorageBackendConfig, error) {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" || id == StorageBackendLocal {
|
||||
return s.localConfig(), nil
|
||||
cfg := s.localConfig()
|
||||
s.applyStoredTestStatus(&cfg)
|
||||
return cfg, nil
|
||||
}
|
||||
var cfg StorageBackendConfig
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
@@ -167,18 +178,13 @@ func (s *StorageService) ListBackendConfigs() ([]StorageBackendConfig, error) {
|
||||
}
|
||||
|
||||
func (s *StorageService) CreateS3Backend(input StorageBackendConfig) (StorageBackendConfig, error) {
|
||||
return s.CreateBackend(input)
|
||||
}
|
||||
|
||||
func (s *StorageService) CreateBackend(input StorageBackendConfig) (StorageBackendConfig, error) {
|
||||
input.ID = randomID(10)
|
||||
input.Provider = normalizeStorageProvider(input.Provider)
|
||||
switch input.Provider {
|
||||
case StorageProviderSFTP:
|
||||
input.Type = StorageBackendSFTP
|
||||
case StorageProviderSMB:
|
||||
input.Type = StorageBackendSMB
|
||||
case StorageProviderWebDAV:
|
||||
input.Type = StorageBackendWebDAV
|
||||
default:
|
||||
input.Type = StorageBackendS3
|
||||
}
|
||||
input.Type = storageTypeForProvider(input.Provider)
|
||||
if err := normalizeStorageBackendConfig(&input, true); err != nil {
|
||||
return StorageBackendConfig{}, err
|
||||
}
|
||||
@@ -193,6 +199,10 @@ func (s *StorageService) CreateS3Backend(input StorageBackendConfig) (StorageBac
|
||||
}
|
||||
|
||||
func (s *StorageService) UpdateS3Backend(id string, input StorageBackendConfig) (StorageBackendConfig, error) {
|
||||
return s.UpdateBackend(id, input)
|
||||
}
|
||||
|
||||
func (s *StorageService) UpdateBackend(id string, input StorageBackendConfig) (StorageBackendConfig, error) {
|
||||
current, err := s.BackendConfig(id)
|
||||
if err != nil {
|
||||
return StorageBackendConfig{}, err
|
||||
@@ -200,18 +210,19 @@ func (s *StorageService) UpdateS3Backend(id string, input StorageBackendConfig)
|
||||
if current.ID == StorageBackendLocal {
|
||||
return StorageBackendConfig{}, fmt.Errorf("local storage cannot be edited")
|
||||
}
|
||||
current.Provider = canonicalStorageProvider(current)
|
||||
current.Type = storageTypeForProvider(current.Provider)
|
||||
|
||||
input.ID = current.ID
|
||||
input.Type = current.Type
|
||||
input.Provider = normalizeStorageProvider(input.Provider)
|
||||
switch input.Provider {
|
||||
case StorageProviderSFTP:
|
||||
input.Type = StorageBackendSFTP
|
||||
case StorageProviderSMB:
|
||||
input.Type = StorageBackendSMB
|
||||
case StorageProviderWebDAV:
|
||||
input.Type = StorageBackendWebDAV
|
||||
default:
|
||||
input.Type = StorageBackendS3
|
||||
requestedProvider := normalizeStorageProvider(input.Provider)
|
||||
requestedType := storageTypeForProvider(requestedProvider)
|
||||
if input.Type != "" && input.Type != requestedType {
|
||||
return StorageBackendConfig{}, fmt.Errorf("storage type cannot be changed after creation")
|
||||
}
|
||||
input.Provider = requestedProvider
|
||||
input.Type = requestedType
|
||||
if input.Provider != current.Provider || input.Type != current.Type {
|
||||
return StorageBackendConfig{}, fmt.Errorf("storage provider cannot be changed after creation")
|
||||
}
|
||||
if strings.TrimSpace(input.SecretKey) == "" {
|
||||
input.SecretKey = current.SecretKey
|
||||
@@ -374,10 +385,56 @@ func (s *StorageService) TestBackend(id string) (StorageBackendConfig, error) {
|
||||
}
|
||||
if cfg.ID != StorageBackendLocal {
|
||||
_ = s.SaveBackendConfig(cfg)
|
||||
} else {
|
||||
_ = s.saveBackendTestStatus(cfg)
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (s *StorageService) applyStoredTestStatus(cfg *StorageBackendConfig) {
|
||||
_ = s.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(storageBackendTestStatusBucket)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
data := bucket.Get([]byte(cfg.ID))
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
var status struct {
|
||||
LastTestedAt time.Time `json:"lastTestedAt,omitempty"`
|
||||
LastTestError string `json:"lastTestError,omitempty"`
|
||||
LastTestSuccess bool `json:"lastTestSuccess,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &status); err != nil {
|
||||
return nil
|
||||
}
|
||||
cfg.LastTestedAt = status.LastTestedAt
|
||||
cfg.LastTestError = status.LastTestError
|
||||
cfg.LastTestSuccess = status.LastTestSuccess
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StorageService) saveBackendTestStatus(cfg StorageBackendConfig) error {
|
||||
status := struct {
|
||||
LastTestedAt time.Time `json:"lastTestedAt,omitempty"`
|
||||
LastTestError string `json:"lastTestError,omitempty"`
|
||||
LastTestSuccess bool `json:"lastTestSuccess,omitempty"`
|
||||
}{
|
||||
LastTestedAt: cfg.LastTestedAt,
|
||||
LastTestError: cfg.LastTestError,
|
||||
LastTestSuccess: cfg.LastTestSuccess,
|
||||
}
|
||||
data, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(storageBackendTestStatusBucket).Put([]byte(cfg.ID), data)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StorageService) backendFromConfig(cfg StorageBackendConfig) (StorageBackend, error) {
|
||||
switch cfg.Type {
|
||||
case StorageBackendLocal:
|
||||
@@ -424,6 +481,35 @@ func normalizeStorageProvider(provider string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalStorageProvider(cfg StorageBackendConfig) string {
|
||||
if cfg.Provider != "" && cfg.Provider != StorageBackendLocal {
|
||||
return normalizeStorageProvider(cfg.Provider)
|
||||
}
|
||||
switch cfg.Type {
|
||||
case StorageBackendSFTP:
|
||||
return StorageProviderSFTP
|
||||
case StorageBackendSMB:
|
||||
return StorageProviderSMB
|
||||
case StorageBackendWebDAV:
|
||||
return StorageProviderWebDAV
|
||||
default:
|
||||
return StorageProviderS3
|
||||
}
|
||||
}
|
||||
|
||||
func storageTypeForProvider(provider string) string {
|
||||
switch normalizeStorageProvider(provider) {
|
||||
case StorageProviderSFTP:
|
||||
return StorageBackendSFTP
|
||||
case StorageProviderSMB:
|
||||
return StorageBackendSMB
|
||||
case StorageProviderWebDAV:
|
||||
return StorageBackendWebDAV
|
||||
default:
|
||||
return StorageBackendS3
|
||||
}
|
||||
}
|
||||
|
||||
func cleanObjectKey(key string) string {
|
||||
return strings.TrimPrefix(filepath.ToSlash(filepath.Clean(strings.TrimPrefix(key, "/"))), "./")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user