Initial Backupper Unraid plugin
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/backupper-unraid/backupper/internal/model"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
dir string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func New(dir string) *Store { return &Store{dir: dir} }
|
||||
|
||||
func (s *Store) Init() error {
|
||||
if err := os.MkdirAll(s.dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(s.configPath()); errors.Is(err, os.ErrNotExist) {
|
||||
return s.SaveConfig(model.DefaultConfig())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) LoadConfig() (model.Config, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var c model.Config
|
||||
if err := readJSON(s.configPath(), &c); err != nil {
|
||||
return c, err
|
||||
}
|
||||
return c, model.ValidateConfig(c)
|
||||
}
|
||||
|
||||
func (s *Store) SaveConfig(c model.Config) error {
|
||||
if err := model.ValidateConfig(c); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return writeJSONAtomic(s.configPath(), c, 0600)
|
||||
}
|
||||
|
||||
func (s *Store) LoadRuns() ([]model.Run, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var runs []model.Run
|
||||
if err := readJSON(s.runsPath(), &runs); errors.Is(err, os.ErrNotExist) {
|
||||
return []model.Run{}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runs, nil
|
||||
}
|
||||
|
||||
func (s *Store) SaveRuns(runs []model.Run) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(runs) > 500 {
|
||||
runs = runs[len(runs)-500:]
|
||||
}
|
||||
return writeJSONAtomic(s.runsPath(), runs, 0600)
|
||||
}
|
||||
|
||||
func (s *Store) configPath() string { return filepath.Join(s.dir, "config.json") }
|
||||
func (s *Store) runsPath() string { return filepath.Join(s.dir, "runs.json") }
|
||||
|
||||
func readJSON(path string, target any) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, target); err != nil {
|
||||
return fmt.Errorf("decode %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSONAtomic(path string, value any, mode os.FileMode) error {
|
||||
b, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b = append(b, '\n')
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), ".backupper-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer os.Remove(tmpName)
|
||||
if err := tmp.Chmod(mode); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := tmp.Write(b); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpName, path)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/backupper-unraid/backupper/internal/model"
|
||||
)
|
||||
|
||||
func TestStoreInitializesAndPersists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s := New(dir)
|
||||
if err := s.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := s.LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.SchemaVersion != model.SchemaVersion {
|
||||
t.Fatalf("schema version = %d", c.SchemaVersion)
|
||||
}
|
||||
info, err := os.Stat(s.configPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != 0600 {
|
||||
t.Fatalf("config mode = %o", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user