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,104 @@
|
||||
package storage
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildSnapraidArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
kind string
|
||||
cfg SnapraidConfig
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
"snapraid_diff",
|
||||
SnapraidConfig{Content: "/pool/snapraid.content"},
|
||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "diff"},
|
||||
},
|
||||
{
|
||||
"snapraid_sync",
|
||||
SnapraidConfig{Content: "/pool/snapraid.content"},
|
||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "sync"},
|
||||
},
|
||||
{
|
||||
"snapraid_check",
|
||||
SnapraidConfig{Content: "/pool/snapraid.content"},
|
||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "check"},
|
||||
},
|
||||
{
|
||||
"snapraid_scrub",
|
||||
SnapraidConfig{Content: "/pool/snapraid.content", ScrubPlan: 8},
|
||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "scrub", "-p", "8"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.kind, func(t *testing.T) {
|
||||
got, err := BuildSnapraidArgs(tt.kind, tt.cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSnapraidArgs: %v", err)
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("BuildSnapraidArgs = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("BuildSnapraidArgs[%d] = %q, want %q", i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSnapraidArgsNoContent(t *testing.T) {
|
||||
_, err := BuildSnapraidArgs("snapraid_sync", SnapraidConfig{Content: ""})
|
||||
if err == nil {
|
||||
t.Error("expected error for empty content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateScrubPlan(t *testing.T) {
|
||||
tests := []struct {
|
||||
plan int
|
||||
wantErr bool
|
||||
}{
|
||||
{0, true},
|
||||
{-1, true},
|
||||
{100, true},
|
||||
{1, false},
|
||||
{50, false},
|
||||
{99, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := ValidateScrubPlan(tt.plan)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Errorf("ValidateScrubPlan(%d) = nil, want error", tt.plan)
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Errorf("ValidateScrubPlan(%d) = %v, want nil", tt.plan, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapraidDataDirs(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{"", nil},
|
||||
{"/disk1", []string{"/disk1"}},
|
||||
{"/disk1,/disk2", []string{"/disk1", "/disk2"}},
|
||||
{"/disk1, /disk2 , /disk3", []string{"/disk1", "/disk2", "/disk3"}},
|
||||
{" /disk1 , /disk2 ", []string{"/disk1", "/disk2"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := ParseSnapraidDataDirs(tt.input)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("ParseSnapraidDataDirs(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
continue
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("ParseSnapraidDataDirs(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user