Mining/pkg/mining/repository.go
Claude f9ecce2e86
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
ax(mining): rename opts/opt to options/option in NewFileRepository
AX Principle 1: predictable names over short names.
`opts` and `opt` are abbreviations — `options` and `option` are unambiguous.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 08:25:14 +01:00

160 lines
3.6 KiB
Go

package mining
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
)
// Repository defines a generic interface for data persistence.
// Implementations can store data in files, databases, etc.
type Repository[T any] interface {
// data, err := repo.Load()
Load() (T, error)
// repo.Save(updated)
Save(data T) error
// repo.Update(func(d *T) error { d.Field = value; return nil })
Update(fn func(*T) error) error
}
// FileRepository provides atomic file-based persistence for JSON data.
// It uses atomic writes (temp file + rename) to prevent corruption.
type FileRepository[T any] struct {
mu sync.RWMutex
path string
defaults func() T
}
// FileRepositoryOption configures a FileRepository.
type FileRepositoryOption[T any] func(*FileRepository[T])
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
func WithDefaults[T any](fn func() T) FileRepositoryOption[T] {
return func(r *FileRepository[T]) {
r.defaults = fn
}
}
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
func NewFileRepository[T any](path string, options ...FileRepositoryOption[T]) *FileRepository[T] {
r := &FileRepository[T]{
path: path,
}
for _, option := range options {
option(r)
}
return r
}
// data, err := repo.Load()
// if err != nil { return defaults, err }
func (r *FileRepository[T]) Load() (T, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var result T
data, err := os.ReadFile(r.path)
if err != nil {
if os.IsNotExist(err) {
if r.defaults != nil {
return r.defaults(), nil
}
return result, nil
}
return result, fmt.Errorf("failed to read file: %w", err)
}
if err := json.Unmarshal(data, &result); err != nil {
return result, fmt.Errorf("failed to unmarshal data: %w", err)
}
return result, nil
}
// if err := repo.Save(updated); err != nil { return fmt.Errorf("save: %w", err) }
func (r *FileRepository[T]) Save(data T) error {
r.mu.Lock()
defer r.mu.Unlock()
return r.saveUnlocked(data)
}
// saveUnlocked saves data without acquiring the lock (caller must hold lock).
func (r *FileRepository[T]) saveUnlocked(data T) error {
dir := filepath.Dir(r.path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
return AtomicWriteFile(r.path, jsonData, 0600)
}
// repo.Update(func(cfg *MinersConfig) error {
// cfg.Miners = append(cfg.Miners, entry)
// return nil
// })
func (r *FileRepository[T]) Update(fn func(*T) error) error {
r.mu.Lock()
defer r.mu.Unlock()
// Load current data
var data T
fileData, err := os.ReadFile(r.path)
if err != nil {
if os.IsNotExist(err) {
if r.defaults != nil {
data = r.defaults()
}
} else {
return fmt.Errorf("failed to read file: %w", err)
}
} else {
if err := json.Unmarshal(fileData, &data); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
}
// Apply modification
if err := fn(&data); err != nil {
return err
}
// Save atomically
return r.saveUnlocked(data)
}
// Path returns the file path of this repository.
func (r *FileRepository[T]) Path() string {
return r.path
}
// Exists returns true if the repository file exists.
func (r *FileRepository[T]) Exists() bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, err := os.Stat(r.path)
return err == nil
}
// Delete removes the repository file.
func (r *FileRepository[T]) Delete() error {
r.mu.Lock()
defer r.mu.Unlock()
err := os.Remove(r.path)
if os.IsNotExist(err) {
return nil
}
return err
}