diff --git a/Makefile b/Makefile index c50e8eb..a4e1060 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BINARY=nasctl -VERSION?=0.3.1 +VERSION?=0.3.2 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/db.go b/internal/db/db.go index 6f62049..45c70f2 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -73,6 +73,12 @@ func (d *DB) Migrate() error { if err := d.runMigration(name, string(content)); err != nil { return fmt.Errorf("apply migration %s: %w", name, err) } + + if name == "0002_nfs_structured.sql" { + if err := d.PopulateLegacyFSIDs(); err != nil { + return fmt.Errorf("populate legacy fsids: %w", err) + } + } } return nil } diff --git a/internal/db/migrations/0004_drop_options.sql b/internal/db/migrations/0004_drop_options.sql new file mode 100644 index 0000000..fe30eba --- /dev/null +++ b/internal/db/migrations/0004_drop_options.sql @@ -0,0 +1,4 @@ +-- Drop the legacy options column after successful migration to structured columns +-- Safe to run on fresh installs (column doesn't exist, silently succeeds in SQLite 3.35+) +-- For SQLite < 3.35 this would fail; nasctl requires modern SQLite. +ALTER TABLE nfs_exports DROP COLUMN options; diff --git a/internal/db/queries_nfs.go b/internal/db/queries_nfs.go index 83e60f3..52f5a44 100644 --- a/internal/db/queries_nfs.go +++ b/internal/db/queries_nfs.go @@ -3,6 +3,10 @@ package db import ( "database/sql" "fmt" + "regexp" + "strconv" + + "github.com/darroyo/nasctl/internal/system" ) func scanNFSExport(row interface { @@ -199,3 +203,55 @@ func (d *DB) FSIDExists(fsid int64) (bool, error) { } return count > 0, nil } + +var fsidLegacyPattern = regexp.MustCompile(`fsid=(\d+)`) + +func (d *DB) PopulateLegacyFSIDs() error { + rows, err := d.conn.Query(`SELECT id, fsid, options FROM nfs_exports WHERE fsid = 0`) + if err != nil { + return fmt.Errorf("query legacy fsids: %w", err) + } + defer rows.Close() + + type pending struct { + id int64 + fsid int64 + } + var updates []pending + + for rows.Next() { + var id, fsid int64 + var options string + if err := rows.Scan(&id, &fsid, &options); err != nil { + return fmt.Errorf("scan row: %w", err) + } + newFSID := int64(0) + + if m := fsidLegacyPattern.FindStringSubmatch(options); m != nil { + if n, err := strconv.ParseInt(m[1], 10, 64); err == nil && n > 0 { + newFSID = n + } + } + + if newFSID == 0 { + n, err := system.GenerateRandomFSID() + if err != nil { + return fmt.Errorf("generate fsid: %w", err) + } + newFSID = int64(n) + } + + updates = append(updates, pending{id, newFSID}) + } + if err := rows.Err(); err != nil { + return fmt.Errorf("rows iteration: %w", err) + } + + for _, u := range updates { + if _, err := d.conn.Exec(`UPDATE nfs_exports SET fsid = ? WHERE id = ?`, u.fsid, u.id); err != nil { + return fmt.Errorf("update fsid for id %d: %w", u.id, err) + } + } + return nil +} + diff --git a/internal/importer/nfs.go b/internal/importer/nfs.go index 3aa4d10..1f5587e 100644 --- a/internal/importer/nfs.go +++ b/internal/importer/nfs.go @@ -3,8 +3,6 @@ package importer import ( "bufio" "bytes" - "crypto/rand" - "encoding/binary" "encoding/json" "fmt" "os" @@ -170,14 +168,10 @@ func parseExportLine(line string) (db.NFSExport, bool) { } func generateImportFSID() (int64, error) { - var b [4]byte - if _, err := rand.Read(b[:]); err != nil { + n, err := system.GenerateRandomFSID() + if err != nil { return 0, err } - n := binary.BigEndian.Uint32(b[:]) - if n == 0 { - n = 1 - } return int64(n), nil } diff --git a/internal/modules/nfs/fsid.go b/internal/modules/nfs/fsid.go index daa101a..d26d600 100644 --- a/internal/modules/nfs/fsid.go +++ b/internal/modules/nfs/fsid.go @@ -1,26 +1,14 @@ package nfs import ( - "crypto/rand" - "encoding/binary" "fmt" -) -func generateRandomFSID() (uint32, error) { - var b [4]byte - if _, err := rand.Read(b[:]); err != nil { - return 0, fmt.Errorf("crypto/rand read: %w", err) - } - n := binary.BigEndian.Uint32(b[:]) - if n == 0 { - n = 1 - } - return n, nil -} + "github.com/darroyo/nasctl/internal/system" +) func UniqueFSID(used map[uint32]struct{}) (uint32, error) { for i := 0; i < 64; i++ { - n, err := generateRandomFSID() + n, err := system.GenerateRandomFSID() if err != nil { return 0, err } diff --git a/internal/modules/nfs/fsid_test.go b/internal/modules/nfs/fsid_test.go index 8510239..a0ed189 100644 --- a/internal/modules/nfs/fsid_test.go +++ b/internal/modules/nfs/fsid_test.go @@ -3,6 +3,8 @@ package nfs import ( "sync" "testing" + + "github.com/darroyo/nasctl/internal/system" ) func TestUniqueFSIDNoCollision(t *testing.T) { @@ -14,7 +16,7 @@ func TestUniqueFSIDNoCollision(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - n, err := generateRandomFSID() + n, err := system.GenerateRandomFSID() if err != nil { t.Errorf("generateRandomFSID failed: %v", err) return diff --git a/internal/system/fsid.go b/internal/system/fsid.go new file mode 100644 index 0000000..049cbd2 --- /dev/null +++ b/internal/system/fsid.go @@ -0,0 +1,19 @@ +package system + +import ( + "crypto/rand" + "encoding/binary" + "fmt" +) + +func GenerateRandomFSID() (uint32, error) { + var b [4]byte + if _, err := rand.Read(b[:]); err != nil { + return 0, fmt.Errorf("crypto/rand read: %w", err) + } + n := binary.BigEndian.Uint32(b[:]) + if n == 0 { + n = 1 + } + return n, nil +}