switch to postgresql
Some checks failed
CI / test-build (push) Has been cancelled

This commit is contained in:
2025-12-16 01:31:27 +07:00
parent 27b0400ef3
commit a7ba6c83ea
10 changed files with 544 additions and 127 deletions

View File

@@ -5,36 +5,75 @@ import (
"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
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)
// 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
}
// Configure connection pool
conn, err := sql.Open("sqlite", dbPath+"?_foreign_keys=1&_journal_mode=WAL")
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
// Set connection pool settings for better performance
conn.SetMaxOpenConns(25) // Maximum number of open connections
conn.SetMaxIdleConns(5) // Maximum number of idle connections
conn.SetConnMaxLifetime(5 * time.Minute) // Maximum connection lifetime
db := &DB{DB: conn}
db := &DB{DB: conn, dbType: dbType}
// Test connection
if err := db.Ping(); err != nil {
@@ -51,114 +90,229 @@ func New(dbPath string) (*DB, error) {
// 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
);
var schema string
-- 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);
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
);
-- 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
);
-- 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 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
);
-- 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
);
-- 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
);
-- 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 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
);
-- 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
);
-- 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
);
-- 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 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 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 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
);
-- 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
);
-- 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);
`
-- 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)

View File

@@ -26,7 +26,7 @@ type Config struct {
Addr string
TemplatesDir string
StaticDir string
DatabasePath string // Path to SQLite database (empty = in-memory mode)
DatabaseConn string // Database connection string (SQLite path or PostgreSQL connection string, empty = in-memory mode)
}
type App struct {
@@ -103,8 +103,8 @@ func New(cfg Config) (*App, error) {
// Initialize database (optional)
var database *db.DB
if cfg.DatabasePath != "" {
dbConn, err := db.New(cfg.DatabasePath)
if cfg.DatabaseConn != "" {
dbConn, err := db.New(cfg.DatabaseConn)
if err != nil {
return nil, fmt.Errorf("init database: %w", err)
}

View File

@@ -32,7 +32,7 @@ func (a *App) handleCreateBackup(w http.ResponseWriter, r *http.Request) {
ISCSITargets: a.iscsiStore.List(),
Policies: a.snapshotPolicy.List(),
Config: map[string]interface{}{
"database_path": a.cfg.DatabasePath,
"database_conn": a.cfg.DatabaseConn,
},
}

View File

@@ -119,7 +119,7 @@ func (a *App) handleSystemInfo(w http.ResponseWriter, r *http.Request) {
if a.database != nil {
info.Database = DatabaseInfo{
Connected: true,
Path: a.cfg.DatabasePath,
Path: a.cfg.DatabaseConn,
}
}

View File

@@ -77,7 +77,7 @@ func (a *App) handleEnableMaintenance(w http.ResponseWriter, r *http.Request) {
ISCSITargets: a.iscsiStore.List(),
Policies: a.snapshotPolicy.List(),
Config: map[string]interface{}{
"database_path": a.cfg.DatabasePath,
"database_conn": a.cfg.DatabaseConn,
},
}