diff --git a/docs/POSTGRESQL_MIGRATION.md b/docs/POSTGRESQL_MIGRATION.md new file mode 100644 index 0000000..086f561 --- /dev/null +++ b/docs/POSTGRESQL_MIGRATION.md @@ -0,0 +1,226 @@ +# PostgreSQL Migration Guide + +## Overview + +AtlasOS now supports both SQLite and PostgreSQL databases. You can switch between them by changing the database connection string. + +## Quick Start + +### Using PostgreSQL + +Set the `ATLAS_DB_CONN` environment variable to a PostgreSQL connection string: + +```bash +export ATLAS_DB_CONN="postgres://username:password@localhost:5432/atlas?sslmode=disable" +./atlas-api +``` + +### Using SQLite (Default) + +Set the `ATLAS_DB_PATH` environment variable to a file path: + +```bash +export ATLAS_DB_PATH="/var/lib/atlas/atlas.db" +./atlas-api +``` + +Or use the connection string format: + +```bash +export ATLAS_DB_CONN="sqlite:///var/lib/atlas/atlas.db" +./atlas-api +``` + +## Connection String Formats + +### PostgreSQL + +``` +postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] +``` + +Examples: +- `postgres://user:pass@localhost:5432/atlas` +- `postgres://user:pass@localhost:5432/atlas?sslmode=disable` +- `postgresql://user:pass@db.example.com:5432/atlas?sslmode=require` + +### SQLite + +- File path: `/var/lib/atlas/atlas.db` +- Connection string: `sqlite:///var/lib/atlas/atlas.db` + +## Setup PostgreSQL Database + +### 1. Install PostgreSQL + +**Ubuntu/Debian:** +```bash +sudo apt-get update +sudo apt-get install postgresql postgresql-contrib +``` + +**CentOS/RHEL:** +```bash +sudo yum install postgresql-server postgresql-contrib +sudo postgresql-setup initdb +sudo systemctl start postgresql +sudo systemctl enable postgresql +``` + +### 2. Create Database and User + +```bash +# Switch to postgres user +sudo -u postgres psql + +# Create database +CREATE DATABASE atlas; + +# Create user +CREATE USER atlas_user WITH PASSWORD 'your_secure_password'; + +# Grant privileges +GRANT ALL PRIVILEGES ON DATABASE atlas TO atlas_user; + +# Exit +\q +``` + +### 3. Configure AtlasOS + +Update your systemd service file (`/etc/systemd/system/atlas-api.service`): + +```ini +[Service] +Environment="ATLAS_DB_CONN=postgres://atlas_user:your_secure_password@localhost:5432/atlas?sslmode=disable" +``` + +Or update `/etc/atlas/atlas.conf`: + +```bash +# PostgreSQL connection string +ATLAS_DB_CONN=postgres://atlas_user:your_secure_password@localhost:5432/atlas?sslmode=disable +``` + +### 4. Restart Service + +```bash +sudo systemctl daemon-reload +sudo systemctl restart atlas-api +``` + +## Migration from SQLite to PostgreSQL + +### Option 1: Fresh Start (Recommended for new installations) + +1. Set up PostgreSQL database (see above) +2. Update connection string +3. Restart service - tables will be created automatically + +### Option 2: Data Migration + +If you have existing SQLite data: + +1. **Export from SQLite:** + ```bash + sqlite3 /var/lib/atlas/atlas.db .dump > atlas_backup.sql + ``` + +2. **Convert SQL to PostgreSQL format:** + - Replace `INTEGER` with `BOOLEAN` for boolean fields + - Replace `TEXT` with `VARCHAR(255)` or `TEXT` as appropriate + - Update timestamp formats + +3. **Import to PostgreSQL:** + ```bash + psql -U atlas_user -d atlas < converted_backup.sql + ``` + +## Rebuilding the Application + +### 1. Install PostgreSQL Development Libraries + +**Ubuntu/Debian:** +```bash +sudo apt-get install libpq-dev +``` + +**CentOS/RHEL:** +```bash +sudo yum install postgresql-devel +``` + +### 2. Update Dependencies + +```bash +go mod tidy +``` + +### 3. Build + +```bash +go build -o atlas-api ./cmd/atlas-api +go build -o atlas-tui ./cmd/atlas-tui +``` + +Or use the installer: + +```bash +sudo ./installer/install.sh +``` + +## Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `ATLAS_DB_CONN` | Database connection string (takes precedence) | `postgres://user:pass@host:5432/db` | +| `ATLAS_DB_PATH` | SQLite database path (fallback if `ATLAS_DB_CONN` not set) | `/var/lib/atlas/atlas.db` | + +## Troubleshooting + +### Connection Refused + +- Check PostgreSQL is running: `sudo systemctl status postgresql` +- Verify connection string format +- Check firewall rules for port 5432 + +### Authentication Failed + +- Verify username and password +- Check `pg_hba.conf` for authentication settings +- Ensure user has proper permissions + +### Database Not Found + +- Verify database exists: `psql -l` +- Check database name in connection string + +### SSL Mode Errors + +- For local connections, use `?sslmode=disable` +- For production, configure SSL properly + +## Performance Considerations + +### PostgreSQL Advantages + +- Better concurrency (multiple writers) +- Advanced query optimization +- Better for high-traffic scenarios +- Supports replication and clustering + +### SQLite Advantages + +- Zero configuration +- Single file deployment +- Lower resource usage +- Perfect for small deployments + +## Schema Differences + +The application automatically handles schema differences: + +- **SQLite**: Uses `INTEGER` for booleans, `TEXT` for strings +- **PostgreSQL**: Uses `BOOLEAN` for booleans, `VARCHAR/TEXT` for strings + +The migration system creates the appropriate schema based on the database type. diff --git a/go.mod b/go.mod index 627da9a..f77fb57 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.24.4 require ( github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/lib/pq v1.10.9 golang.org/x/crypto v0.46.0 + modernc.org/sqlite v1.40.1 ) require ( @@ -18,5 +20,4 @@ require ( modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.40.1 // indirect ) diff --git a/go.sum b/go.sum index 8538477..ebe5843 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -14,14 +18,38 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/installer/install.sh b/installer/install.sh index 47ef384..ae8fe8f 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -621,13 +621,17 @@ import ( func main() { addr := env("ATLAS_HTTP_ADDR", ":8080") - dbPath := env("ATLAS_DB_PATH", "data/atlas.db") + // Support both ATLAS_DB_PATH (SQLite) and ATLAS_DB_CONN (PostgreSQL connection string) + dbConn := env("ATLAS_DB_CONN", "") + if dbConn == "" { + dbConn = env("ATLAS_DB_PATH", "data/atlas.db") + } app, err := httpapp.New(httpapp.Config{ Addr: addr, TemplatesDir: "web/templates", StaticDir: "web/static", - DatabasePath: dbPath, + DatabaseConn: dbConn, }) if err != nil { log.Fatalf("init app: %v", err) @@ -878,6 +882,7 @@ SyslogIdentifier=atlas-api # Environment variables Environment="ATLAS_HTTP_ADDR=$HTTP_ADDR" Environment="ATLAS_DB_PATH=$DB_PATH" +Environment="ATLAS_DB_CONN=$DB_PATH" Environment="ATLAS_BACKUP_DIR=$BACKUP_DIR" Environment="ATLAS_LOG_LEVEL=INFO" Environment="ATLAS_LOG_FORMAT=json" @@ -910,8 +915,11 @@ create_config() { # HTTP Server ATLAS_HTTP_ADDR=$HTTP_ADDR -# Database +# Database (SQLite path or PostgreSQL connection string) +# For SQLite: ATLAS_DB_PATH=/var/lib/atlas/atlas.db +# For PostgreSQL: ATLAS_DB_CONN=postgres://user:pass@host:port/dbname?sslmode=disable ATLAS_DB_PATH=$DB_PATH +ATLAS_DB_CONN= # Backup Directory ATLAS_BACKUP_DIR=$BACKUP_DIR diff --git a/internal/db/db.go b/internal/db/db.go index 119e6ef..05f69e7 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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) diff --git a/internal/httpapp/app.go b/internal/httpapp/app.go index f95344f..b404879 100644 --- a/internal/httpapp/app.go +++ b/internal/httpapp/app.go @@ -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) } diff --git a/internal/httpapp/backup_handlers.go b/internal/httpapp/backup_handlers.go index 7ee6b60..9657533 100644 --- a/internal/httpapp/backup_handlers.go +++ b/internal/httpapp/backup_handlers.go @@ -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, }, } diff --git a/internal/httpapp/diagnostics_handlers.go b/internal/httpapp/diagnostics_handlers.go index 2ed8861..5bae856 100644 --- a/internal/httpapp/diagnostics_handlers.go +++ b/internal/httpapp/diagnostics_handlers.go @@ -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, } } diff --git a/internal/httpapp/maintenance_handlers.go b/internal/httpapp/maintenance_handlers.go index fae11be..641b578 100644 --- a/internal/httpapp/maintenance_handlers.go +++ b/internal/httpapp/maintenance_handlers.go @@ -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, }, } diff --git a/test/integration_test.go b/test/integration_test.go index a160789..ae422aa 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -28,7 +28,7 @@ func NewTestServer(t *testing.T) *TestServer { Addr: ":0", // Use random port TemplatesDir: templatesDir, StaticDir: staticDir, - DatabasePath: "", // Empty = in-memory mode (no database) + DatabaseConn: "", // Empty = in-memory mode (no database) }) if err != nil { t.Fatalf("create test app: %v", err)