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

141 lines
4.7 KiB
Go

package main
import (
"flag"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/darroyo/nasctl/internal/db"
"github.com/darroyo/nasctl/internal/engine"
"github.com/darroyo/nasctl/internal/importer"
"github.com/darroyo/nasctl/internal/modules/nfs"
"github.com/darroyo/nasctl/internal/modules/samba"
"github.com/darroyo/nasctl/internal/modules/users"
"github.com/darroyo/nasctl/internal/storage"
"github.com/darroyo/nasctl/internal/web"
)
func main() {
addr := flag.String("addr", envOrDefault("NASCTL_ADDR", ":8080"), "HTTP listen address")
dbPath := flag.String("db", envOrDefault("NASCTL_DB", "/var/lib/nasctl/nasctl.db"), "SQLite database path")
smbConfPath := flag.String("smb-conf", envOrDefault("NASCTL_SMB_CONF", "/etc/samba/smb.conf"), "Samba config output path")
exportsPath := flag.String("exports", envOrDefault("NASCTL_EXPORTS", "/etc/exports"), "NFS exports output path")
execSystem := flag.Bool("exec-system", envOrDefault("NASCTL_EXEC_SYSTEM", "true") == "true", "Execute real system commands (reload services, useradd, etc.)")
allowedRoots := flag.String("allowed-roots", envOrDefault("NASCTL_ALLOWED_ROOTS", ""), "Comma-separated allowed root dirs for shares/exports (empty = any absolute path)")
adminUser := flag.String("admin-user", envOrDefault("NASCTL_ADMIN_USER", "admin"), "Initial admin username (only used if no admin exists)")
adminPass := flag.String("admin-pass", envOrDefault("NASCTL_ADMIN_PASSWORD", "admin"), "Initial admin password (only used if no admin exists)")
importOnBoot := flag.Bool("import-on-boot", envOrDefault("NASCTL_IMPORT_ON_BOOT", "false") == "true", "Import existing smb.conf and /etc/exports on first boot")
uploadMaxBytes := flag.Int64("upload-max-bytes", parseEnvInt64("NASCTL_UPLOAD_MAX_BYTES", 104857600), "Max bytes for file uploads")
previewMaxBytes := flag.Int64("preview-max-bytes", parseEnvInt64("NASCTL_PREVIEW_MAX_BYTES", 262144), "Max bytes for file previews")
flag.Parse()
if err := os.MkdirAll(filepath.Dir(*dbPath), 0o755); err != nil {
log.Fatalf("create db dir: %v", err)
}
database, err := db.Open(*dbPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer database.Close()
if err := database.Migrate(); err != nil {
log.Fatalf("migrate db: %v", err)
}
jm := storage.NewJobManager(database, *execSystem)
if err := jm.ResetOrphans(); err != nil {
log.Printf("reset orphaned storage jobs: %v", err)
}
auth, err := web.NewAuthService(database)
if err != nil {
log.Fatalf("init auth: %v", err)
}
created, err := auth.EnsureAdmin(*adminUser, *adminPass)
if err != nil {
log.Fatalf("ensure admin: %v", err)
}
if created {
log.Printf("created initial admin user %q — change the password after first login", *adminUser)
}
if *importOnBoot {
result := importer.ImportOnBoot(nil, database, *smbConfPath, *exportsPath, *adminUser)
if result.SambaImported > 0 {
log.Printf("[importer] imported %d existing samba shares — review and apply", result.SambaImported)
}
if result.NFSImported > 0 {
log.Printf("[importer] imported %d existing nfs exports — review and apply", result.NFSImported)
}
if result.UsersImported > 0 {
log.Printf("[importer] imported %d system users — review and apply", result.UsersImported)
}
}
sambaModule := samba.New(samba.Config{
SMBConfPath: *smbConfPath,
Reload: *execSystem,
})
nfsModule := nfs.New(nfs.Config{
ExportsPath: *exportsPath,
Reload: *execSystem,
})
usersModule := users.New(users.Config{
Execute: *execSystem,
CreateHome: true,
})
eng := engine.New(sambaModule, nfsModule, usersModule)
srv := web.NewServer(database, eng, web.Options{
AllowedRoots: parseRoots(*allowedRoots),
Auth: auth,
SMBConfPath: *smbConfPath,
ExportsPath: *exportsPath,
AdminUsername: *adminUser,
UploadMaxBytes: *uploadMaxBytes,
PreviewMaxBytes: *previewMaxBytes,
JobManager: jm,
})
log.Printf("nasctl %s (commit %s) listening on %s (db=%s exec-system=%v)", web.Version, web.Commit, *addr, *dbPath, *execSystem)
if err := http.ListenAndServe(*addr, srv.Handler()); err != nil {
log.Fatalf("server error: %v", err)
}
}
func parseRoots(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
parts := strings.Split(raw, ",")
roots := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
roots = append(roots, p)
}
}
return roots
}
func envOrDefault(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
func parseEnvInt64(key string, fallback int64) int64 {
if value := os.Getenv(key); value != "" {
if v, err := strconv.ParseInt(value, 10, 64); err == nil {
return v
}
}
return fallback
}