package db import ( "database/sql" "fmt" "os" "path/filepath" "strings" "time" _ "github.com/lib/pq" _ "modernc.org/sqlite" ) // DB wraps a database connection type DB struct { *sql.DB dbType string // "sqlite" or "postgres" } // New creates a new database connection // dbConn can be: // - SQLite: file path (e.g., "/var/lib/atlas/atlas.db") or "sqlite:///path/to/db" // - PostgreSQL: connection string (e.g., "postgres://user:pass@host:port/dbname?sslmode=disable") func New(dbConn string) (*DB, error) { var ( conn *sql.DB dbType string err error ) // Detect database type from connection string if strings.HasPrefix(dbConn, "postgres://") || strings.HasPrefix(dbConn, "postgresql://") { // PostgreSQL connection dbType = "postgres" conn, err = sql.Open("postgres", dbConn) if err != nil { return nil, fmt.Errorf("open postgres database: %w", err) } // PostgreSQL connection pool settings conn.SetMaxOpenConns(25) conn.SetMaxIdleConns(5) conn.SetConnMaxLifetime(5 * time.Minute) } else { // SQLite connection (default or explicit) dbType = "sqlite" dbPath := dbConn // Remove sqlite:// prefix if present if strings.HasPrefix(dbPath, "sqlite://") { dbPath = strings.TrimPrefix(dbPath, "sqlite://") // Handle sqlite:///path/to/db format if strings.HasPrefix(dbPath, "///") { dbPath = dbPath[2:] // Remove one / } else if strings.HasPrefix(dbPath, "//") { dbPath = dbPath[1:] // Remove one / } } // Ensure directory exists for SQLite dir := filepath.Dir(dbPath) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create db directory: %w", err) } // Configure connection pool conn, err = sql.Open("sqlite", dbPath+"?_foreign_keys=1&_journal_mode=WAL") if err != nil { return nil, fmt.Errorf("open sqlite database: %w", err) } // SQLite connection pool settings (SQLite has different limits) conn.SetMaxOpenConns(1) // SQLite doesn't support multiple writers well conn.SetMaxIdleConns(1) conn.SetConnMaxLifetime(0) // Keep connections open } db := &DB{DB: conn, dbType: dbType} // 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 { var schema string if db.dbType == "postgres" { // PostgreSQL schema schema = ` -- Users table CREATE TABLE IF NOT EXISTS users ( id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255), password_hash TEXT NOT NULL, role VARCHAR(50) NOT NULL, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Audit logs table CREATE TABLE IF NOT EXISTS audit_logs ( id VARCHAR(255) PRIMARY KEY, actor VARCHAR(255) NOT NULL, action VARCHAR(100) NOT NULL, resource VARCHAR(255) NOT NULL, result VARCHAR(50) NOT NULL, message TEXT, ip VARCHAR(45), user_agent TEXT, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); 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 VARCHAR(255) PRIMARY KEY, name VARCHAR(255) UNIQUE NOT NULL, path TEXT NOT NULL, dataset VARCHAR(255) NOT NULL, description TEXT, read_only BOOLEAN NOT NULL DEFAULT false, guest_ok BOOLEAN NOT NULL DEFAULT false, enabled BOOLEAN NOT NULL DEFAULT true ); -- SMB share valid users (many-to-many) CREATE TABLE IF NOT EXISTS smb_share_users ( share_id VARCHAR(255) NOT NULL, username VARCHAR(255) 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 VARCHAR(255) PRIMARY KEY, path TEXT UNIQUE NOT NULL, dataset VARCHAR(255) NOT NULL, read_only BOOLEAN NOT NULL DEFAULT false, root_squash BOOLEAN NOT NULL DEFAULT true, enabled BOOLEAN NOT NULL DEFAULT true ); -- NFS export clients (many-to-many) CREATE TABLE IF NOT EXISTS nfs_export_clients ( export_id VARCHAR(255) NOT NULL, client VARCHAR(255) 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 VARCHAR(255) PRIMARY KEY, iqn VARCHAR(255) UNIQUE NOT NULL, enabled BOOLEAN NOT NULL DEFAULT true ); -- iSCSI target initiators (many-to-many) CREATE TABLE IF NOT EXISTS iscsi_target_initiators ( target_id VARCHAR(255) NOT NULL, initiator VARCHAR(255) 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 VARCHAR(255) NOT NULL, lun_id INTEGER NOT NULL, zvol VARCHAR(255) NOT NULL, size BIGINT NOT NULL, backend VARCHAR(50) 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 VARCHAR(255) PRIMARY KEY, dataset VARCHAR(255) NOT NULL, schedule_type VARCHAR(50) NOT NULL, schedule_value VARCHAR(255), retention_count INTEGER, retention_days INTEGER, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_snapshot_policy_dataset ON snapshot_policies(dataset); ` } else { // SQLite schema (original) 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() }