Improve dashboard health and state recovery
This commit is contained in:
+41
-7
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.casaderoll.de/michael/urbm/internal/model"
|
||||
)
|
||||
@@ -51,12 +52,20 @@ func (s *Store) SaveConfig(c model.Config) error {
|
||||
}
|
||||
|
||||
func (s *Store) LoadRuns() ([]model.Run, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var runs []model.Run
|
||||
if err := readJSON(s.runsPath(), &runs); errors.Is(err, os.ErrNotExist) {
|
||||
return []model.Run{}, nil
|
||||
} else if err != nil {
|
||||
var decodeErr *jsonDecodeError
|
||||
if errors.As(err, &decodeErr) {
|
||||
quarantined, quarantineErr := quarantineFile(s.runsPath())
|
||||
if quarantineErr != nil {
|
||||
return nil, fmt.Errorf("%w; quarantine failed: %v", err, quarantineErr)
|
||||
}
|
||||
return nil, fmt.Errorf("%w; corrupt file moved to %s", err, quarantined)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return runs, nil
|
||||
@@ -72,8 +81,8 @@ func (s *Store) SaveRuns(runs []model.Run) error {
|
||||
}
|
||||
|
||||
func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
path := s.backupMetricsPath()
|
||||
b, err := os.ReadFile(path)
|
||||
legacySource := false
|
||||
@@ -95,7 +104,7 @@ func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
|
||||
if b[0] == '[' {
|
||||
var metrics []model.BackupMetric
|
||||
if err := json.Unmarshal(b, &metrics); err != nil {
|
||||
return nil, true, fmt.Errorf("decode %s: %w", path, err)
|
||||
return nil, true, quarantineDecodeError(path, err)
|
||||
}
|
||||
return metrics, true, nil
|
||||
}
|
||||
@@ -108,7 +117,7 @@ func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
|
||||
}
|
||||
var metric model.BackupMetric
|
||||
if err := json.Unmarshal(line, &metric); err != nil {
|
||||
return nil, legacySource, fmt.Errorf("decode %s line %d: %w", path, index+1, err)
|
||||
return nil, legacySource, quarantineDecodeError(path, fmt.Errorf("line %d: %w", index+1, err))
|
||||
}
|
||||
metrics = append(metrics, metric)
|
||||
}
|
||||
@@ -169,11 +178,36 @@ func readJSON(path string, target any) error {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, target); err != nil {
|
||||
return fmt.Errorf("decode %s: %w", path, err)
|
||||
return &jsonDecodeError{path: path, err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonDecodeError struct {
|
||||
path string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *jsonDecodeError) Error() string { return fmt.Sprintf("decode %s: %v", e.path, e.err) }
|
||||
func (e *jsonDecodeError) Unwrap() error { return e.err }
|
||||
|
||||
func quarantineDecodeError(path string, decodeErr error) error {
|
||||
quarantined, err := quarantineFile(path)
|
||||
wrapped := &jsonDecodeError{path: path, err: decodeErr}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w; quarantine failed: %v", wrapped, err)
|
||||
}
|
||||
return fmt.Errorf("%w; corrupt file moved to %s", wrapped, quarantined)
|
||||
}
|
||||
|
||||
func quarantineFile(path string) (string, error) {
|
||||
quarantined := path + ".corrupt-" + time.Now().UTC().Format("20060102-150405.000000000")
|
||||
if err := os.Rename(path, quarantined); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return quarantined, nil
|
||||
}
|
||||
|
||||
func writeJSONAtomic(path string, value any, mode os.FileMode) error {
|
||||
b, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user