diff --git a/Makefile b/Makefile index fb2fd06..7a7d461 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BINARY=nasctl -VERSION?=0.7.6 +VERSION?=0.8.0 GO?=go LDFLAGS=-s -w -X github.com/darroyo/nasctl/internal/web.Version=$(VERSION) -X github.com/darroyo/nasctl/internal/web.Commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) BUILD_FLAGS=CGO_ENABLED=0 diff --git a/internal/db/migrations/0010_rename_snapraid_conf.sql b/internal/db/migrations/0010_rename_snapraid_conf.sql new file mode 100644 index 0000000..c9eca22 --- /dev/null +++ b/internal/db/migrations/0010_rename_snapraid_conf.sql @@ -0,0 +1,4 @@ +-- Rename snapraid_content to snapraid_conf to reflect that this field +-- holds the path to the snapraid.conf TEXT configuration file, +-- not the binary snapraid.content database file. +ALTER TABLE storage_config RENAME COLUMN snapraid_content TO snapraid_conf; diff --git a/internal/db/models.go b/internal/db/models.go index 0bec8da..2d36fbb 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -97,7 +97,7 @@ type StorageConfig struct { MoverInplace bool `json:"mover_inplace"` MoverRsyncOptions string `json:"mover_rsync_options"` MoverWarningThreshold int `json:"mover_warning_threshold"` - SnapraidContent string `json:"snapraid_content"` + SnapraidConf string `json:"snapraid_conf"` SnapraidDataDirs string `json:"snapraid_data_dirs"` SnapraidParityDir string `json:"snapraid_parity_dir"` SnapraidScrubPlan int `json:"snapraid_scrub_plan"` diff --git a/internal/db/queries_storage.go b/internal/db/queries_storage.go index 16e682d..512e4b5 100644 --- a/internal/db/queries_storage.go +++ b/internal/db/queries_storage.go @@ -19,7 +19,7 @@ func scanStorageConfig(row interface { &removeSource, &inplace, &c.MoverRsyncOptions, - &c.SnapraidContent, + &c.SnapraidConf, &c.SnapraidDataDirs, &c.SnapraidParityDir, &c.SnapraidScrubPlan, @@ -36,7 +36,7 @@ func scanStorageConfig(row interface { } func (d *DB) GetStorageConfig() (StorageConfig, error) { - row := d.conn.QueryRow(`SELECT id, mover_source, mover_dest, mover_clean_macos, mover_remove_source, mover_inplace, mover_rsync_options, snapraid_content, snapraid_data_dirs, snapraid_parity_dir, snapraid_scrub_plan, mover_warning_threshold, updated_at FROM storage_config WHERE id = 1`) + row := d.conn.QueryRow(`SELECT id, mover_source, mover_dest, mover_clean_macos, mover_remove_source, mover_inplace, mover_rsync_options, snapraid_conf, snapraid_data_dirs, snapraid_parity_dir, snapraid_scrub_plan, mover_warning_threshold, updated_at FROM storage_config WHERE id = 1`) c, err := scanStorageConfig(row) if err == sql.ErrNoRows { return StorageConfig{ID: 1, MoverCleanMacOS: true, MoverRemoveSource: true, MoverInplace: true, SnapraidScrubPlan: 8}, nil @@ -51,11 +51,11 @@ func (d *DB) UpdateStorageConfig(c StorageConfig) error { _, err := d.conn.Exec(` UPDATE storage_config SET mover_source=?, mover_dest=?, mover_clean_macos=?, mover_remove_source=?, mover_inplace=?, - mover_rsync_options=?, snapraid_content=?, snapraid_data_dirs=?, snapraid_parity_dir=?, + mover_rsync_options=?, snapraid_conf=?, snapraid_data_dirs=?, snapraid_parity_dir=?, snapraid_scrub_plan=?, mover_warning_threshold=?, updated_at=datetime('now') WHERE id=1`, c.MoverSource, c.MoverDest, toInt(c.MoverCleanMacOS), toInt(c.MoverRemoveSource), toInt(c.MoverInplace), - c.MoverRsyncOptions, c.SnapraidContent, c.SnapraidDataDirs, c.SnapraidParityDir, + c.MoverRsyncOptions, c.SnapraidConf, c.SnapraidDataDirs, c.SnapraidParityDir, c.SnapraidScrubPlan, c.MoverWarningThreshold, c.ID, ) if err != nil { diff --git a/internal/storage/jobs.go b/internal/storage/jobs.go index c7bd859..86628a5 100644 --- a/internal/storage/jobs.go +++ b/internal/storage/jobs.go @@ -170,13 +170,13 @@ func (jm *JobManager) runJob(job db.StorageJob) { return } sc := SnapraidConfig{ - Content: envcfg.SnapraidContent, + Conf: envcfg.SnapraidConf, DataDirs: ParseSnapraidDataDirs(envcfg.SnapraidDataDirs), ParityDir: envcfg.SnapraidParityDir, ScrubPlan: envcfg.SnapraidScrubPlan, } - if err := ValidateSnapraidContent(sc.Content); err != nil { - jm.failJob(job.ID, -1, fmt.Sprintf("content validation: %v", err)) + if err := ValidateSnapraidConf(sc.Conf); err != nil { + jm.failJob(job.ID, -1, fmt.Sprintf("conf validation: %v", err)) return } if job.Kind == "snapraid_scrub" { diff --git a/internal/storage/snapraid.go b/internal/storage/snapraid.go index 29928df..c18eab0 100644 --- a/internal/storage/snapraid.go +++ b/internal/storage/snapraid.go @@ -8,17 +8,17 @@ import ( ) type SnapraidConfig struct { - Content string - DataDirs []string + Conf 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") + if cfg.Conf == "" { + return nil, fmt.Errorf("snapraid conf file is required") } - args := []string{"snapraid", "-c", cfg.Content} + args := []string{"snapraid", "-c", cfg.Conf} switch kind { case "snapraid_diff": args = append(args, "diff") @@ -34,16 +34,16 @@ func BuildSnapraidArgs(kind string, cfg SnapraidConfig) ([]string, error) { return args, nil } -func ValidateSnapraidContent(content string) error { - if content == "" { - return fmt.Errorf("snapraid content file is required") +func ValidateSnapraidConf(conf string) error { + if conf == "" { + return fmt.Errorf("snapraid conf file is required") } - info, err := os.Stat(content) + info, err := os.Stat(conf) if err != nil { - return fmt.Errorf("snapraid content %s: %w", content, err) + return fmt.Errorf("snapraid conf %s: %w", conf, err) } if info.IsDir() { - return fmt.Errorf("snapraid content %s: is a directory, not a file", content) + return fmt.Errorf("snapraid conf %s: is a directory, not a file", conf) } return nil } diff --git a/internal/storage/snapraid_test.go b/internal/storage/snapraid_test.go index 8afcbc2..4ffa271 100644 --- a/internal/storage/snapraid_test.go +++ b/internal/storage/snapraid_test.go @@ -10,23 +10,23 @@ func TestBuildSnapraidArgs(t *testing.T) { }{ { "snapraid_diff", - SnapraidConfig{Content: "/pool/snapraid.content"}, - []string{"snapraid", "-c", "/pool/snapraid.content", "diff"}, + SnapraidConfig{Conf: "/etc/snapraid.conf"}, + []string{"snapraid", "-c", "/etc/snapraid.conf", "diff"}, }, { "snapraid_sync", - SnapraidConfig{Content: "/pool/snapraid.content"}, - []string{"snapraid", "-c", "/pool/snapraid.content", "sync"}, + SnapraidConfig{Conf: "/etc/snapraid.conf"}, + []string{"snapraid", "-c", "/etc/snapraid.conf", "sync"}, }, { "snapraid_check", - SnapraidConfig{Content: "/pool/snapraid.content"}, - []string{"snapraid", "-c", "/pool/snapraid.content", "check"}, + SnapraidConfig{Conf: "/etc/snapraid.conf"}, + []string{"snapraid", "-c", "/etc/snapraid.conf", "check"}, }, { "snapraid_scrub", - SnapraidConfig{Content: "/pool/snapraid.content", ScrubPlan: 8}, - []string{"snapraid", "-c", "/pool/snapraid.content", "scrub", "-p", "8"}, + SnapraidConfig{Conf: "/etc/snapraid.conf", ScrubPlan: 8}, + []string{"snapraid", "-c", "/etc/snapraid.conf", "scrub", "-p", "8"}, }, } for _, tt := range tests { @@ -48,10 +48,10 @@ func TestBuildSnapraidArgs(t *testing.T) { } } -func TestBuildSnapraidArgsNoContent(t *testing.T) { - _, err := BuildSnapraidArgs("snapraid_sync", SnapraidConfig{Content: ""}) +func TestBuildSnapraidArgsNoConf(t *testing.T) { + _, err := BuildSnapraidArgs("snapraid_sync", SnapraidConfig{Conf: ""}) if err == nil { - t.Error("expected error for empty content") + t.Error("expected error for empty conf") } } diff --git a/internal/web/handlers_storage.go b/internal/web/handlers_storage.go index c254865..9a308c7 100644 --- a/internal/web/handlers_storage.go +++ b/internal/web/handlers_storage.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "os" "strconv" "strings" "time" @@ -31,9 +32,36 @@ func (s *Server) handleStorageGetConfig(w http.ResponseWriter, r *http.Request) writeError(w, http.StatusInternalServerError, err.Error()) return } + if cfg.SnapraidConf == "" { + for _, path := range []string{"/etc/snapraid.conf", "/usr/local/etc/snapraid.conf"} { + if info, err := os.Stat(path); err == nil && !info.IsDir() { + cfg.SnapraidConf = path + break + } + } + } writeJSON(w, http.StatusOK, cfg) } +func (s *Server) handleStorageGetConfFile(w http.ResponseWriter, r *http.Request) { + cfg, err := s.DB.GetStorageConfig() + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + if cfg.SnapraidConf == "" { + writeError(w, http.StatusBadRequest, "snapraid conf path not configured") + return + } + data, err := os.ReadFile(cfg.SnapraidConf) + if err != nil { + writeError(w, http.StatusInternalServerError, fmt.Sprintf("read %s: %v", cfg.SnapraidConf, err)) + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write(data) +} + func (s *Server) handleStorageUpdateConfig(w http.ResponseWriter, r *http.Request) { var cfg db.StorageConfig defer r.Body.Close() @@ -67,9 +95,9 @@ func (s *Server) handleStorageUpdateConfig(w http.ResponseWriter, r *http.Reques return } } - if cfg.SnapraidContent != "" { - if err := validateAbsPath(cfg.SnapraidContent); err != nil { - writeError(w, http.StatusBadRequest, fmt.Sprintf("snapraid_content: %v", err)) + if cfg.SnapraidConf != "" { + if err := validateAbsPath(cfg.SnapraidConf); err != nil { + writeError(w, http.StatusBadRequest, fmt.Sprintf("snapraid_conf: %v", err)) return } } diff --git a/internal/web/router.go b/internal/web/router.go index b6069ad..9865075 100644 --- a/internal/web/router.go +++ b/internal/web/router.go @@ -92,6 +92,7 @@ func NewRouter(s *Server) chi.Router { storageRouter.Get("/disk-usage", s.handleStorageDiskUsage) storageRouter.Get("/config", s.handleStorageGetConfig) storageRouter.Put("/config", s.handleStorageUpdateConfig) + storageRouter.Get("/conf-file", s.handleStorageGetConfFile) storageRouter.Route("/jobs", func(j chi.Router) { j.Get("/", s.handleStorageListJobs) j.Post("/", s.handleStorageStartJob) diff --git a/web/src/api.ts b/web/src/api.ts index dccc6a4..bbc614d 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -170,7 +170,7 @@ export interface StorageConfig { mover_inplace: boolean; mover_rsync_options: string; mover_warning_threshold: number; - snapraid_content: string; + snapraid_conf: string; snapraid_data_dirs: string; snapraid_parity_dir: string; snapraid_scrub_plan: number; @@ -322,6 +322,10 @@ export const api = { request("POST", "/storage/jobs", { kind, args }), cancelStorageJob: (id: number) => request<{ ok: boolean }>("POST", `/storage/jobs/${id}/cancel`), storageJobStreamUrl: (id: number) => `/api/storage/jobs/${id}/stream`, + getStorageConfFile: () => fetch(`/api/storage/conf-file`).then(r => { + if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`); + return r.text(); + }), }; export function formatBytes(bytes: number): string { diff --git a/web/src/pages/Storage.tsx b/web/src/pages/Storage.tsx index cbd97f3..1debea4 100644 --- a/web/src/pages/Storage.tsx +++ b/web/src/pages/Storage.tsx @@ -41,7 +41,10 @@ export default function Storage() { const [scrubPlanInput, setScrubPlanInput] = useState("8"); const [dirtyConfig, setDirtyConfig] = useState(false); const [pendingConfig, setPendingConfig] = useState>({}); - const outputRef = useRef(null); + const [confModalOpen, setConfModalOpen] = useState(false); + const [confModalContent, setConfModalContent] = useState(""); + const [confModalError, setConfModalError] = useState(""); + const outputRef = useRef(null); const esRef = useRef(null); useEffect(() => { @@ -116,6 +119,18 @@ export default function Storage() { } } + async function openConfModal() { + setConfModalOpen(true); + setConfModalContent(""); + setConfModalError(""); + try { + const content = await api.getStorageConfFile(); + setConfModalContent(content); + } catch (e: unknown) { + setConfModalError(e instanceof Error ? e.message : String(e)); + } + } + async function startJob(kind: JobKind) { try { const job = await api.startStorageJob(kind); @@ -311,12 +326,35 @@ export default function Storage() {
SnapRAID - handleConfigChange("snapraid_content", v)} - placeholder="/mnt/pool/snapraid.content" - /> +
+ handleConfigChange("snapraid_conf", v)} + placeholder="/etc/snapraid.conf" + /> +
+ + {!mergedConfig.snapraid_conf && ( + + No configurado. Busca en{" "} + /etc/snapraid.conf o{" "} + /usr/local/etc/snapraid.conf + + )} +
+

+ Ruta al archivo snapraid.conf. Este es el archivo de configuración de texto,{" "} + no el archivo binario .content. +

+
)} + + {/* Conf file viewer modal */} + {confModalOpen && ( +
+
+
+

+ {mergedConfig.snapraid_conf ?? "snapraid.conf"} +

+ +
+ {confModalError ? ( +

{confModalError}

+ ) : ( +
+                {confModalContent || "Cargando..."}
+              
+ )} +
+
+ )} ); }