feat(storage): rename snapraid_content to snapraid_conf; clarify conf vs content file; add auto-detect and conf viewer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -8,17 +8,17 @@ import (
|
||||
)
|
||||
|
||||
type SnapraidConfig struct {
|
||||
Content 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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+5
-1
@@ -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<StorageJob>("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 {
|
||||
|
||||
@@ -41,7 +41,10 @@ export default function Storage() {
|
||||
const [scrubPlanInput, setScrubPlanInput] = useState("8");
|
||||
const [dirtyConfig, setDirtyConfig] = useState(false);
|
||||
const [pendingConfig, setPendingConfig] = useState<Partial<StorageConfig>>({});
|
||||
const outputRef = useRef<HTMLPreElement>(null);
|
||||
const [confModalOpen, setConfModalOpen] = useState(false);
|
||||
const [confModalContent, setConfModalContent] = useState("");
|
||||
const [confModalError, setConfModalError] = useState("");
|
||||
const outputRef = useRef<HTMLPreElement | null>(null);
|
||||
const esRef = useRef<EventSource | null>(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() {
|
||||
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="text-sm font-medium text-slate-300">SnapRAID</legend>
|
||||
<div>
|
||||
<Field
|
||||
label="Content file"
|
||||
value={mergedConfig.snapraid_content ?? ""}
|
||||
onChange={(v) => handleConfigChange("snapraid_content", v)}
|
||||
placeholder="/mnt/pool/snapraid.content"
|
||||
label="Archivo de configuración"
|
||||
value={mergedConfig.snapraid_conf ?? ""}
|
||||
onChange={(v) => handleConfigChange("snapraid_conf", v)}
|
||||
placeholder="/etc/snapraid.conf"
|
||||
/>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-slate-500 underline hover:text-slate-300 disabled:opacity-40"
|
||||
onClick={openConfModal}
|
||||
disabled={!mergedConfig.snapraid_conf}
|
||||
>
|
||||
Ver contenido
|
||||
</button>
|
||||
{!mergedConfig.snapraid_conf && (
|
||||
<span className="text-xs text-amber-400">
|
||||
No configurado. Busca en{" "}
|
||||
<code className="text-amber-300">/etc/snapraid.conf</code> o{" "}
|
||||
<code className="text-amber-300">/usr/local/etc/snapraid.conf</code>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Ruta al archivo <code className="text-slate-400">snapraid.conf</code>. Este es el archivo de configuración de texto,{" "}
|
||||
<span className="text-amber-400">no el archivo binario .content</span>.
|
||||
</p>
|
||||
</div>
|
||||
<Field
|
||||
label="Directorios de datos (CSV)"
|
||||
value={mergedConfig.snapraid_data_dirs ?? ""}
|
||||
@@ -503,6 +541,32 @@ export default function Storage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Conf file viewer modal */}
|
||||
{confModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div className="w-full max-w-2xl rounded-lg border border-slate-600 bg-slate-800 p-4 shadow-xl">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-slate-200">
|
||||
{mergedConfig.snapraid_conf ?? "snapraid.conf"}
|
||||
</h3>
|
||||
<button
|
||||
className="text-slate-400 hover:text-white"
|
||||
onClick={() => setConfModalOpen(false)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{confModalError ? (
|
||||
<p className="text-sm text-red-400">{confModalError}</p>
|
||||
) : (
|
||||
<pre className="max-h-96 overflow-auto rounded bg-slate-900 p-3 text-xs text-slate-300 font-mono">
|
||||
{confModalContent || "Cargando..."}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user