v0.9.0: Storage paths registry, per-app HDD_PATH resolution, storage management UI
- Fix backup toggles not appearing (read each app's own HDD_PATH from app.yaml) - Storage paths registry in settings.json with auto-discovery from deployed apps - Settings page "Adattárolók" section with disk usage, add/remove/default/schedulable - Deploy page path field as dropdown of registered storage paths - Health check storage monitoring (mount point, disk usage alerts) - Mount-point validation utilities (Linux syscall + cross-platform stubs) - Controller docker-compose mount changed to /mnt:/mnt:rw for multi-storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings holds customer-modifiable overrides and cached state.
|
||||
@@ -27,6 +29,9 @@ type Settings struct {
|
||||
|
||||
// Per-app backup preferences
|
||||
AppBackup map[string]AppBackupPrefs `json:"app_backup,omitempty"`
|
||||
|
||||
// Storage paths registry
|
||||
StoragePaths []StoragePath `json:"storage_paths,omitempty"`
|
||||
}
|
||||
|
||||
// AppBackupPrefs holds per-app backup toggle state.
|
||||
@@ -34,6 +39,15 @@ type AppBackupPrefs struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// StoragePath represents a registered external storage location.
|
||||
type StoragePath struct {
|
||||
Path string `json:"path"` // e.g., "/mnt/hdd_1"
|
||||
Label string `json:"label,omitempty"` // e.g., "Külső HDD 1TB"
|
||||
IsDefault bool `json:"is_default,omitempty"` // new apps use this by default
|
||||
Schedulable bool `json:"schedulable"` // whether new apps can be deployed here
|
||||
AddedAt string `json:"added_at"` // RFC3339
|
||||
}
|
||||
|
||||
// NotificationPrefs holds customer notification preferences.
|
||||
type NotificationPrefs struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
@@ -224,3 +238,161 @@ func (s *Settings) SetAppBackupBulk(prefs map[string]bool) error {
|
||||
}
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// --- Storage Paths ---
|
||||
|
||||
// GetStoragePaths returns a copy of all registered storage paths.
|
||||
func (s *Settings) GetStoragePaths() []StoragePath {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.StoragePaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]StoragePath, len(s.StoragePaths))
|
||||
copy(result, s.StoragePaths)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetDefaultStoragePath returns the default storage path string, or "".
|
||||
func (s *Settings) GetDefaultStoragePath() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, sp := range s.StoragePaths {
|
||||
if sp.IsDefault {
|
||||
return sp.Path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSchedulableStoragePaths returns paths available for new deployments.
|
||||
func (s *Settings) GetSchedulableStoragePaths() []StoragePath {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var result []StoragePath
|
||||
for _, sp := range s.StoragePaths {
|
||||
if sp.Schedulable {
|
||||
result = append(result, sp)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AddStoragePath registers a new storage path. Validation is done by caller.
|
||||
func (s *Settings) AddStoragePath(sp StoragePath) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if sp.IsDefault {
|
||||
for i := range s.StoragePaths {
|
||||
s.StoragePaths[i].IsDefault = false
|
||||
}
|
||||
}
|
||||
s.StoragePaths = append(s.StoragePaths, sp)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// RemoveStoragePath removes a path by its path string.
|
||||
func (s *Settings) RemoveStoragePath(path string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var kept []StoragePath
|
||||
for _, sp := range s.StoragePaths {
|
||||
if sp.Path != path {
|
||||
kept = append(kept, sp)
|
||||
}
|
||||
}
|
||||
s.StoragePaths = kept
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// SetDefaultStoragePath changes which path is the default.
|
||||
func (s *Settings) SetDefaultStoragePath(path string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
found := false
|
||||
for i := range s.StoragePaths {
|
||||
if s.StoragePaths[i].Path == path {
|
||||
s.StoragePaths[i].IsDefault = true
|
||||
found = true
|
||||
} else {
|
||||
s.StoragePaths[i].IsDefault = false
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("storage path %q not found", path)
|
||||
}
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// SetSchedulable enables/disables a path for new deployments.
|
||||
func (s *Settings) SetSchedulable(path string, schedulable bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i := range s.StoragePaths {
|
||||
if s.StoragePaths[i].Path == path {
|
||||
s.StoragePaths[i].Schedulable = schedulable
|
||||
return s.save()
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("storage path %q not found", path)
|
||||
}
|
||||
|
||||
// AutoDiscoverStoragePaths scans for HDD_PATH values and registers them if none exist.
|
||||
// discoveredPaths are pre-scanned HDD_PATH values from deployed apps' app.yaml.
|
||||
// fallbackHDDPath is the legacy controller.yaml paths.hdd_path (may be empty).
|
||||
func (s *Settings) AutoDiscoverStoragePaths(discoveredPaths []string, fallbackHDDPath string, logger *log.Logger) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if len(s.StoragePaths) > 0 {
|
||||
return // already configured
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
var ordered []string
|
||||
for _, p := range discoveredPaths {
|
||||
cleaned := filepath.Clean(p)
|
||||
if cleaned != "" && !seen[cleaned] {
|
||||
seen[cleaned] = true
|
||||
ordered = append(ordered, cleaned)
|
||||
}
|
||||
}
|
||||
if fallbackHDDPath != "" {
|
||||
cleaned := filepath.Clean(fallbackHDDPath)
|
||||
if !seen[cleaned] {
|
||||
seen[cleaned] = true
|
||||
ordered = append(ordered, cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
for i, path := range ordered {
|
||||
sp := StoragePath{
|
||||
Path: path,
|
||||
Label: InferStorageLabel(path),
|
||||
IsDefault: i == 0,
|
||||
Schedulable: true,
|
||||
AddedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
s.StoragePaths = append(s.StoragePaths, sp)
|
||||
}
|
||||
|
||||
if len(s.StoragePaths) > 0 {
|
||||
if err := s.save(); err != nil {
|
||||
logger.Printf("[ERROR] Failed to save auto-discovered storage paths: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Printf("[INFO] Auto-discovered %d storage path(s)", len(s.StoragePaths))
|
||||
for _, sp := range s.StoragePaths {
|
||||
logger.Printf("[INFO] %s (%s) default=%v", sp.Path, sp.Label, sp.IsDefault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InferStorageLabel generates a human-readable label for a storage path.
|
||||
func InferStorageLabel(path string) string {
|
||||
base := filepath.Base(path)
|
||||
if strings.HasPrefix(base, "hdd") || strings.HasPrefix(base, "ssd") || strings.HasPrefix(base, "usb") {
|
||||
return fmt.Sprintf("Külső tárhely (%s)", base)
|
||||
}
|
||||
return fmt.Sprintf("Tárhely (%s)", base)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user