Security: - Add HTTP Basic/Digest authentication middleware (enable via MINING_API_AUTH env) - Fix WebSocket origin check with proper URL parsing - Add max limit (10000) to remote log lines request - Improve CLI args validation with stricter patterns Networking: - Fix WebSocket double-close with sync.Once in PeerConnection - Add 10s dial timeout for WebSocket connections - Reset write deadline after failed sends - Fix handler race in Transport.OnMessage with RWMutex - Make EventHub.Stop() idempotent, buffer channels to prevent goroutine leaks Code Simplification: - Extract AtomicWriteFile helper to reduce duplication across 4 files - Remove redundant MinerTypeRegistry, use MinerFactory instead - Register simulated miner in MinerFactory - Remove dead portToString() code from manager.go Documentation: - Add Advanced API Authentication section to FUTURE_IDEAS.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
158 lines
3.6 KiB
Go
158 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 {
|
|
// Load reads data from the repository
|
|
Load() (T, error)
|
|
|
|
// Save writes data to the repository
|
|
Save(data T) error
|
|
|
|
// Update atomically loads, modifies, and saves data
|
|
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])
|
|
|
|
// WithDefaults sets the default value factory for when the file doesn't exist.
|
|
func WithDefaults[T any](fn func() T) FileRepositoryOption[T] {
|
|
return func(r *FileRepository[T]) {
|
|
r.defaults = fn
|
|
}
|
|
}
|
|
|
|
// NewFileRepository creates a new file-based repository.
|
|
func NewFileRepository[T any](path string, opts ...FileRepositoryOption[T]) *FileRepository[T] {
|
|
r := &FileRepository[T]{
|
|
path: path,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Load reads and deserializes data from the file.
|
|
// Returns defaults if file doesn't exist.
|
|
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
|
|
}
|
|
|
|
// Save serializes and writes data to the file atomically.
|
|
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)
|
|
}
|
|
|
|
// Update atomically loads, modifies, and saves data.
|
|
// The modification function receives a pointer to the data.
|
|
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
|
|
}
|