27a52d2986
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
114 lines
3.1 KiB
Go
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])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|