fix: populate fsid for legacy NFS exports on migration
Migration 0002 added fsid column with DEFAULT 0 but never populated existing rows. Now PopulateLegacyFSIDs() runs after 0002 to extract fsid from legacy options string or generate random. Also: move generateRandomFSID to internal/system to avoid import cycles. Migration 0004 drops the legacy options column. Version: 0.3.1 -> 0.3.2
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
BINARY=nasctl
|
BINARY=nasctl
|
||||||
VERSION?=0.3.1
|
VERSION?=0.3.2
|
||||||
GO?=go
|
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)
|
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
|
BUILD_FLAGS=CGO_ENABLED=0
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ func (d *DB) Migrate() error {
|
|||||||
if err := d.runMigration(name, string(content)); err != nil {
|
if err := d.runMigration(name, string(content)); err != nil {
|
||||||
return fmt.Errorf("apply migration %s: %w", name, err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -3,6 +3,10 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/darroyo/nasctl/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func scanNFSExport(row interface {
|
func scanNFSExport(row interface {
|
||||||
@@ -199,3 +203,55 @@ func (d *DB) FSIDExists(fsid int64) (bool, error) {
|
|||||||
}
|
}
|
||||||
return count > 0, nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package importer
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -170,14 +168,10 @@ func parseExportLine(line string) (db.NFSExport, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateImportFSID() (int64, error) {
|
func generateImportFSID() (int64, error) {
|
||||||
var b [4]byte
|
n, err := system.GenerateRandomFSID()
|
||||||
if _, err := rand.Read(b[:]); err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
n := binary.BigEndian.Uint32(b[:])
|
|
||||||
if n == 0 {
|
|
||||||
n = 1
|
|
||||||
}
|
|
||||||
return int64(n), nil
|
return int64(n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
package nfs
|
package nfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
|
||||||
|
|
||||||
func generateRandomFSID() (uint32, error) {
|
"github.com/darroyo/nasctl/internal/system"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func UniqueFSID(used map[uint32]struct{}) (uint32, error) {
|
func UniqueFSID(used map[uint32]struct{}) (uint32, error) {
|
||||||
for i := 0; i < 64; i++ {
|
for i := 0; i < 64; i++ {
|
||||||
n, err := generateRandomFSID()
|
n, err := system.GenerateRandomFSID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package nfs
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/darroyo/nasctl/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUniqueFSIDNoCollision(t *testing.T) {
|
func TestUniqueFSIDNoCollision(t *testing.T) {
|
||||||
@@ -14,7 +16,7 @@ func TestUniqueFSIDNoCollision(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
n, err := generateRandomFSID()
|
n, err := system.GenerateRandomFSID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("generateRandomFSID failed: %v", err)
|
t.Errorf("generateRandomFSID failed: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user