feat: add mergerfs mover and snapraid integration
New 'Almacenamiento' page with:
- Auto-detection of rsync, mergerfs, snapraid binaries and mergerfs mount
- Configurable pool settings (source/dest, macOS cleanup, rsync flags)
- Mergerfs mover with dry-run preview and live SSE output streaming
- SnapRAID diff/sync/scrub/check with live SSE output
- Async job system (1 concurrent job) with SSE streaming
- Job history table
Backend:
- internal/storage/ package with capabilities, mergerfs, snapraid, jobs
- storage_config and storage_jobs DB tables (migration 0008)
- GET/PUT /api/storage/config, GET /api/storage/capabilities
- POST/GET /api/storage/jobs, GET /api/storage/jobs/{id}/stream
- Storage operations disabled when NASCTL_EXEC_SYSTEM=false
Closes #new-feature
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SnapraidConfig struct {
|
||||
Content string
|
||||
DataDirs []string
|
||||
ParityDir string
|
||||
ScrubPlan int
|
||||
}
|
||||
|
||||
func BuildSnapraidArgs(kind string, cfg SnapraidConfig) ([]string, error) {
|
||||
if cfg.Content == "" {
|
||||
return nil, fmt.Errorf("snapraid content file is required")
|
||||
}
|
||||
args := []string{"snapraid", "-c", cfg.Content}
|
||||
switch kind {
|
||||
case "snapraid_diff":
|
||||
args = append(args, "diff")
|
||||
case "snapraid_sync":
|
||||
args = append(args, "sync")
|
||||
case "snapraid_scrub":
|
||||
args = append(args, "scrub", "-p", strconv.Itoa(cfg.ScrubPlan))
|
||||
case "snapraid_check":
|
||||
args = append(args, "check")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown snapraid kind: %s", kind)
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func ValidateSnapraidContent(content string) error {
|
||||
if content == "" {
|
||||
return fmt.Errorf("snapraid content file is required")
|
||||
}
|
||||
info, err := os.Stat(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("snapraid content %s: %w", content, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("snapraid content %s: is a directory, not a file", content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateScrubPlan(plan int) error {
|
||||
if plan < 1 || plan > 99 {
|
||||
return fmt.Errorf("scrub plan must be between 1 and 99, got %d", plan)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseSnapraidDataDirs(raw string) []string {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var dirs []string
|
||||
for _, d := range strings.Split(raw, ",") {
|
||||
d = strings.TrimSpace(d)
|
||||
if d != "" {
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
Reference in New Issue
Block a user