package db import ( "database/sql" "fmt" "os" "path/filepath" _ "modernc.org/sqlite" ) // DB wraps a database connection type DB struct { *sql.DB } // New creates a new database connection func New(dbPath string) (*DB, error) { // Ensure directory exists dir := filepath.Dir(dbPath) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create db directory: %w", err) } conn, err := sql.Open("sqlite", dbPath+"?_foreign_keys=1") if err != nil { return nil, fmt.Errorf("open database: %w", err) } db := &DB{DB: conn} // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping database: %w", err) } // Run migrations if err := db.migrate(); err != nil { return nil, fmt.Errorf("migrate database: %w", err) } return db, nil } // migrate runs database migrations func (db *DB) migrate() error { schema := ` -- Users table CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); -- Audit logs table CREATE TABLE IF NOT EXISTS audit_logs ( id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, resource TEXT NOT NULL, result TEXT NOT NULL, message TEXT, ip TEXT, user_agent TEXT, timestamp TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_logs(actor); CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_resource ON audit_logs(resource); CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_logs(timestamp); -- SMB shares table CREATE TABLE IF NOT EXISTS smb_shares ( id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, path TEXT NOT NULL, dataset TEXT NOT NULL, description TEXT, read_only INTEGER NOT NULL DEFAULT 0, guest_ok INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1 ); -- SMB share valid users (many-to-many) CREATE TABLE IF NOT EXISTS smb_share_users ( share_id TEXT NOT NULL, username TEXT NOT NULL, PRIMARY KEY (share_id, username), FOREIGN KEY (share_id) REFERENCES smb_shares(id) ON DELETE CASCADE ); -- NFS exports table CREATE TABLE IF NOT EXISTS nfs_exports ( id TEXT PRIMARY KEY, path TEXT UNIQUE NOT NULL, dataset TEXT NOT NULL, read_only INTEGER NOT NULL DEFAULT 0, root_squash INTEGER NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1 ); -- NFS export clients (many-to-many) CREATE TABLE IF NOT EXISTS nfs_export_clients ( export_id TEXT NOT NULL, client TEXT NOT NULL, PRIMARY KEY (export_id, client), FOREIGN KEY (export_id) REFERENCES nfs_exports(id) ON DELETE CASCADE ); -- iSCSI targets table CREATE TABLE IF NOT EXISTS iscsi_targets ( id TEXT PRIMARY KEY, iqn TEXT UNIQUE NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 ); -- iSCSI target initiators (many-to-many) CREATE TABLE IF NOT EXISTS iscsi_target_initiators ( target_id TEXT NOT NULL, initiator TEXT NOT NULL, PRIMARY KEY (target_id, initiator), FOREIGN KEY (target_id) REFERENCES iscsi_targets(id) ON DELETE CASCADE ); -- iSCSI LUNs table CREATE TABLE IF NOT EXISTS iscsi_luns ( target_id TEXT NOT NULL, lun_id INTEGER NOT NULL, zvol TEXT NOT NULL, size INTEGER NOT NULL, backend TEXT NOT NULL DEFAULT 'zvol', PRIMARY KEY (target_id, lun_id), FOREIGN KEY (target_id) REFERENCES iscsi_targets(id) ON DELETE CASCADE ); -- Snapshot policies table CREATE TABLE IF NOT EXISTS snapshot_policies ( id TEXT PRIMARY KEY, dataset TEXT NOT NULL, schedule_type TEXT NOT NULL, schedule_value TEXT, retention_count INTEGER, retention_days INTEGER, enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_snapshot_policy_dataset ON snapshot_policies(dataset); ` if _, err := db.Exec(schema); err != nil { return fmt.Errorf("create schema: %w", err) } return nil } // Close closes the database connection func (db *DB) Close() error { return db.DB.Close() }