package db import ( "database/sql" "fmt" "regexp" "strconv" "github.com/darroyo/nasctl/internal/system" ) func scanNFSExport(row interface { Scan(dest ...any) error }) (NFSExport, error) { var export NFSExport var clients, createdAt, updatedAt string var readOnly, async_, rootSquash, subtreeCheck, fsid int if err := row.Scan( &export.ID, &export.Path, &clients, &readOnly, &async_, &rootSquash, &subtreeCheck, &fsid, &export.Advanced, &createdAt, &updatedAt, ); err != nil { return NFSExport{}, err } var err error export.Clients, err = decodeJSONStrings(clients) if err != nil { return NFSExport{}, err } export.ReadOnly = readOnly != 0 export.Async = async_ != 0 export.RootSquash = rootSquash != 0 export.SubtreeCheck = subtreeCheck != 0 export.FSID = int64(fsid) export.CreatedAt = parseTime(createdAt) export.UpdatedAt = parseTime(updatedAt) return export, nil } func (d *DB) ListNFSExports() ([]NFSExport, error) { rows, err := d.conn.Query(` SELECT id, path, clients, read_only, async_, root_squash, subtree_check, fsid, advanced, created_at, updated_at FROM nfs_exports ORDER BY path ASC`) if err != nil { return nil, fmt.Errorf("list nfs exports: %w", err) } defer rows.Close() var exports []NFSExport for rows.Next() { export, err := scanNFSExport(rows) if err != nil { return nil, fmt.Errorf("scan nfs export: %w", err) } exports = append(exports, export) } return exports, rows.Err() } func (d *DB) GetNFSExport(id int64) (NFSExport, error) { row := d.conn.QueryRow(` SELECT id, path, clients, read_only, async_, root_squash, subtree_check, fsid, advanced, created_at, updated_at FROM nfs_exports WHERE id = ?`, id) export, err := scanNFSExport(row) if err == sql.ErrNoRows { return NFSExport{}, fmt.Errorf("nfs export not found") } if err != nil { return NFSExport{}, fmt.Errorf("get nfs export: %w", err) } return export, nil } func (d *DB) CreateNFSExport(export NFSExport) (NFSExport, error) { clients, err := encodeJSONStrings(export.Clients) if err != nil { return NFSExport{}, err } readOnly := 0 if export.ReadOnly { readOnly = 1 } async_ := 0 if export.Async { async_ = 1 } rootSquash := 0 if export.RootSquash { rootSquash = 1 } subtreeCheck := 0 if export.SubtreeCheck { subtreeCheck = 1 } advanced := export.Advanced if advanced == "" { advanced = "{}" } result, err := d.conn.Exec(` INSERT INTO nfs_exports (path, clients, read_only, async_, root_squash, subtree_check, fsid, advanced) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, export.Path, clients, readOnly, async_, rootSquash, subtreeCheck, export.FSID, advanced, ) if err != nil { return NFSExport{}, fmt.Errorf("create nfs export: %w", err) } id, err := result.LastInsertId() if err != nil { return NFSExport{}, fmt.Errorf("last insert id: %w", err) } return d.GetNFSExport(id) } func (d *DB) UpdateNFSExport(id int64, export NFSExport) (NFSExport, error) { clients, err := encodeJSONStrings(export.Clients) if err != nil { return NFSExport{}, err } readOnly := 0 if export.ReadOnly { readOnly = 1 } async_ := 0 if export.Async { async_ = 1 } rootSquash := 0 if export.RootSquash { rootSquash = 1 } subtreeCheck := 0 if export.SubtreeCheck { subtreeCheck = 1 } advanced := export.Advanced if advanced == "" { advanced = "{}" } result, err := d.conn.Exec(` UPDATE nfs_exports SET path = ?, clients = ?, read_only = ?, async_ = ?, root_squash = ?, subtree_check = ?, fsid = ?, advanced = ?, updated_at = datetime('now') WHERE id = ?`, export.Path, clients, readOnly, async_, rootSquash, subtreeCheck, export.FSID, advanced, id, ) if err != nil { return NFSExport{}, fmt.Errorf("update nfs export: %w", err) } rows, err := result.RowsAffected() if err != nil { return NFSExport{}, err } if rows == 0 { return NFSExport{}, fmt.Errorf("nfs export not found") } return d.GetNFSExport(id) } func (d *DB) DeleteNFSExport(id int64) error { result, err := d.conn.Exec(`DELETE FROM nfs_exports WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete nfs export: %w", err) } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return fmt.Errorf("nfs export not found") } return nil } func (d *DB) GetAllFSIDs() ([]int64, error) { rows, err := d.conn.Query(`SELECT fsid FROM nfs_exports WHERE fsid != 0`) if err != nil { return nil, fmt.Errorf("get all fsids: %w", err) } defer rows.Close() var fsids []int64 for rows.Next() { var fsid int64 if err := rows.Scan(&fsid); err != nil { return nil, err } fsids = append(fsids, fsid) } return fsids, rows.Err() } func (d *DB) FSIDExists(fsid int64) (bool, error) { var count int err := d.conn.QueryRow(`SELECT COUNT(1) FROM nfs_exports WHERE fsid = ?`, fsid).Scan(&count) if err != nil { return false, err } 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 }