Files
darroyo 27a52d2986 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
2026-07-06 23:23:22 -04:00

114 lines
3.1 KiB
Go

package storage
import "testing"
func TestBuildPreviewArgs(t *testing.T) {
cfg := MoverConfig{Source: "/mnt/ssd", Dest: "/mnt/pool"}
args, err := BuildPreviewArgs(cfg)
if err != nil {
t.Fatalf("BuildPreviewArgs: %v", err)
}
if args[0] != "rsync" {
t.Errorf("args[0] = %q, want rsync", args[0])
}
if args[1] != "-an" {
t.Errorf("args[1] = %q, want -an", args[1])
}
if args[2] != "--remove-source-files" {
t.Errorf("args[2] = %q, want --remove-source-files", args[2])
}
if args[3] != "--out-format=%n" {
t.Errorf("args[3] = %q, want --out-format=%%n", args[3])
}
if args[4] != "/mnt/ssd/" {
t.Errorf("args[4] = %q, want /mnt/ssd/", args[4])
}
if args[5] != "/mnt/pool/" {
t.Errorf("args[5] = %q, want /mnt/pool/", args[5])
}
}
func TestBuildMoverArgs(t *testing.T) {
cfg := MoverConfig{Source: "/mnt/ssd", Dest: "/mnt/pool", Inplace: false, RemoveSrc: true}
args, err := BuildMoverArgs(cfg)
if err != nil {
t.Fatalf("BuildMoverArgs: %v", err)
}
if args[0] != "rsync" {
t.Errorf("args[0] = %q, want rsync", args[0])
}
foundRemove := false
for _, a := range args {
if a == "--remove-source-files" {
foundRemove = true
}
}
if !foundRemove {
t.Errorf("--remove-source-files not found in args %v", args)
}
}
func TestBuildMoverArgsExtraFlags(t *testing.T) {
cfg := MoverConfig{Source: "/src", Dest: "/dst", ExtraFlags: []string{"--exclude=*.tmp", "--max-size=2G"}}
args, err := BuildMoverArgs(cfg)
if err != nil {
t.Fatalf("BuildMoverArgs with extra flags: %v", err)
}
if args[0] != "rsync" {
t.Errorf("args[0] = %q, want rsync", args[0])
}
idx := -1
for i, a := range args {
if a == "--exclude=*.tmp" {
idx = i
}
}
if idx == -1 {
t.Errorf("--exclude=*.tmp not found in args %v", args)
}
}
func TestParseExtraRsyncFlags(t *testing.T) {
tests := []struct {
name string
input string
want []string
wantErr bool
}{
{"empty", "", nil, false},
{"single flag", "--exclude=*.tmp", []string{"--exclude=*.tmp"}, false},
{"multiple flags", "--exclude=*.tmp\n--max-size=2G", []string{"--exclude=*.tmp", "--max-size=2G"}, false},
{"comment and empty", "# comment\n\n--exclude=*.tmp\n", []string{"--exclude=*.tmp"}, false},
{"missing dash", "exclude=*.tmp", nil, true},
{"forbidden char semicolon", "--flag;echo", nil, true},
{"forbidden char pipe", "--flag|grep", nil, true},
{"forbidden char dollar", "--flag$var", nil, true},
{"forbidden char backtick", "--flag`cmd`", nil, true},
{"newline in flag", "--flag\nline", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseExtraRsyncFlags(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("ParseExtraRsyncFlags(%q) = %v, want error", tt.input, got)
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if len(got) != len(tt.want) {
t.Errorf("ParseExtraRsyncFlags(%q) = %v, want %v", tt.input, got, tt.want)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("ParseExtraRsyncFlags(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
}
})
}
}