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

@@ -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.

3
go.mod
View File

@@ -4,7 +4,9 @@ go 1.24.4
require ( require (
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0
modernc.org/sqlite v1.40.1
) )
require ( require (
@@ -18,5 +20,4 @@ require (
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.40.1 // indirect
) )

28
go.sum
View File

@@ -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/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 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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= 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/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 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/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 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/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 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 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 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= 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=

View File

@@ -621,13 +621,17 @@ import (
func main() { func main() {
addr := env("ATLAS_HTTP_ADDR", ":8080") 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{ app, err := httpapp.New(httpapp.Config{
Addr: addr, Addr: addr,
TemplatesDir: "web/templates", TemplatesDir: "web/templates",
StaticDir: "web/static", StaticDir: "web/static",
DatabasePath: dbPath, DatabaseConn: dbConn,
}) })
if err != nil { if err != nil {
log.Fatalf("init app: %v", err) log.Fatalf("init app: %v", err)
@@ -878,6 +882,7 @@ SyslogIdentifier=atlas-api
# Environment variables # Environment variables
Environment="ATLAS_HTTP_ADDR=$HTTP_ADDR" Environment="ATLAS_HTTP_ADDR=$HTTP_ADDR"
Environment="ATLAS_DB_PATH=$DB_PATH" Environment="ATLAS_DB_PATH=$DB_PATH"
Environment="ATLAS_DB_CONN=$DB_PATH"
Environment="ATLAS_BACKUP_DIR=$BACKUP_DIR" Environment="ATLAS_BACKUP_DIR=$BACKUP_DIR"
Environment="ATLAS_LOG_LEVEL=INFO" Environment="ATLAS_LOG_LEVEL=INFO"
Environment="ATLAS_LOG_FORMAT=json" Environment="ATLAS_LOG_FORMAT=json"
@@ -910,8 +915,11 @@ create_config() {
# HTTP Server # HTTP Server
ATLAS_HTTP_ADDR=$HTTP_ADDR 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_PATH=$DB_PATH
ATLAS_DB_CONN=
# Backup Directory # Backup Directory
ATLAS_BACKUP_DIR=$BACKUP_DIR ATLAS_BACKUP_DIR=$BACKUP_DIR

View File

@@ -5,36 +5,75 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
_ "github.com/lib/pq"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
// DB wraps a database connection // DB wraps a database connection
type DB struct { type DB struct {
*sql.DB *sql.DB
dbType string // "sqlite" or "postgres"
} }
// New creates a new database connection // New creates a new database connection
func New(dbPath string) (*DB, error) { // dbConn can be:
// Ensure directory exists // - SQLite: file path (e.g., "/var/lib/atlas/atlas.db") or "sqlite:///path/to/db"
dir := filepath.Dir(dbPath) // - PostgreSQL: connection string (e.g., "postgres://user:pass@host:port/dbname?sslmode=disable")
if err := os.MkdirAll(dir, 0755); err != nil { func New(dbConn string) (*DB, error) {
return nil, fmt.Errorf("create db directory: %w", err) 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 db := &DB{DB: conn, dbType: dbType}
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}
// Test connection // Test connection
if err := db.Ping(); err != nil { if err := db.Ping(); err != nil {
@@ -51,114 +90,229 @@ func New(dbPath string) (*DB, error) {
// migrate runs database migrations // migrate runs database migrations
func (db *DB) migrate() error { func (db *DB) migrate() error {
schema := ` var schema string
-- 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 if db.dbType == "postgres" {
CREATE TABLE IF NOT EXISTS audit_logs ( // PostgreSQL schema
id TEXT PRIMARY KEY, schema = `
actor TEXT NOT NULL, -- Users table
action TEXT NOT NULL, CREATE TABLE IF NOT EXISTS users (
resource TEXT NOT NULL, id VARCHAR(255) PRIMARY KEY,
result TEXT NOT NULL, username VARCHAR(255) UNIQUE NOT NULL,
message TEXT, email VARCHAR(255),
ip TEXT, password_hash TEXT NOT NULL,
user_agent TEXT, role VARCHAR(50) NOT NULL,
timestamp TEXT NOT NULL active BOOLEAN NOT NULL DEFAULT true,
); created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_logs(actor); updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
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 -- Audit logs table
CREATE TABLE IF NOT EXISTS smb_shares ( CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
name TEXT UNIQUE NOT NULL, actor VARCHAR(255) NOT NULL,
path TEXT NOT NULL, action VARCHAR(100) NOT NULL,
dataset TEXT NOT NULL, resource VARCHAR(255) NOT NULL,
description TEXT, result VARCHAR(50) NOT NULL,
read_only INTEGER NOT NULL DEFAULT 0, message TEXT,
guest_ok INTEGER NOT NULL DEFAULT 0, ip VARCHAR(45),
enabled INTEGER NOT NULL DEFAULT 1 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) -- SMB shares table
CREATE TABLE IF NOT EXISTS smb_share_users ( CREATE TABLE IF NOT EXISTS smb_shares (
share_id TEXT NOT NULL, id VARCHAR(255) PRIMARY KEY,
username TEXT NOT NULL, name VARCHAR(255) UNIQUE NOT NULL,
PRIMARY KEY (share_id, username), path TEXT NOT NULL,
FOREIGN KEY (share_id) REFERENCES smb_shares(id) ON DELETE CASCADE 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 -- SMB share valid users (many-to-many)
CREATE TABLE IF NOT EXISTS nfs_exports ( CREATE TABLE IF NOT EXISTS smb_share_users (
id TEXT PRIMARY KEY, share_id VARCHAR(255) NOT NULL,
path TEXT UNIQUE NOT NULL, username VARCHAR(255) NOT NULL,
dataset TEXT NOT NULL, PRIMARY KEY (share_id, username),
read_only INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (share_id) REFERENCES smb_shares(id) ON DELETE CASCADE
root_squash INTEGER NOT NULL DEFAULT 1, );
enabled INTEGER NOT NULL DEFAULT 1
);
-- NFS export clients (many-to-many) -- NFS exports table
CREATE TABLE IF NOT EXISTS nfs_export_clients ( CREATE TABLE IF NOT EXISTS nfs_exports (
export_id TEXT NOT NULL, id VARCHAR(255) PRIMARY KEY,
client TEXT NOT NULL, path TEXT UNIQUE NOT NULL,
PRIMARY KEY (export_id, client), dataset VARCHAR(255) NOT NULL,
FOREIGN KEY (export_id) REFERENCES nfs_exports(id) ON DELETE CASCADE read_only BOOLEAN NOT NULL DEFAULT false,
); root_squash BOOLEAN NOT NULL DEFAULT true,
enabled BOOLEAN NOT NULL DEFAULT true
);
-- iSCSI targets table -- NFS export clients (many-to-many)
CREATE TABLE IF NOT EXISTS iscsi_targets ( CREATE TABLE IF NOT EXISTS nfs_export_clients (
id TEXT PRIMARY KEY, export_id VARCHAR(255) NOT NULL,
iqn TEXT UNIQUE NOT NULL, client VARCHAR(255) NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1 PRIMARY KEY (export_id, client),
); FOREIGN KEY (export_id) REFERENCES nfs_exports(id) ON DELETE CASCADE
);
-- iSCSI target initiators (many-to-many) -- iSCSI targets table
CREATE TABLE IF NOT EXISTS iscsi_target_initiators ( CREATE TABLE IF NOT EXISTS iscsi_targets (
target_id TEXT NOT NULL, id VARCHAR(255) PRIMARY KEY,
initiator TEXT NOT NULL, iqn VARCHAR(255) UNIQUE NOT NULL,
PRIMARY KEY (target_id, initiator), enabled BOOLEAN NOT NULL DEFAULT true
FOREIGN KEY (target_id) REFERENCES iscsi_targets(id) ON DELETE CASCADE );
);
-- iSCSI LUNs table -- iSCSI target initiators (many-to-many)
CREATE TABLE IF NOT EXISTS iscsi_luns ( CREATE TABLE IF NOT EXISTS iscsi_target_initiators (
target_id TEXT NOT NULL, target_id VARCHAR(255) NOT NULL,
lun_id INTEGER NOT NULL, initiator VARCHAR(255) NOT NULL,
zvol TEXT NOT NULL, PRIMARY KEY (target_id, initiator),
size INTEGER NOT NULL, FOREIGN KEY (target_id) REFERENCES iscsi_targets(id) ON DELETE CASCADE
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 -- iSCSI LUNs table
CREATE TABLE IF NOT EXISTS snapshot_policies ( CREATE TABLE IF NOT EXISTS iscsi_luns (
id TEXT PRIMARY KEY, target_id VARCHAR(255) NOT NULL,
dataset TEXT NOT NULL, lun_id INTEGER NOT NULL,
schedule_type TEXT NOT NULL, zvol VARCHAR(255) NOT NULL,
schedule_value TEXT, size BIGINT NOT NULL,
retention_count INTEGER, backend VARCHAR(50) NOT NULL DEFAULT 'zvol',
retention_days INTEGER, PRIMARY KEY (target_id, lun_id),
enabled INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (target_id) REFERENCES iscsi_targets(id) ON DELETE CASCADE
created_at TEXT NOT NULL, );
updated_at TEXT NOT NULL
); -- Snapshot policies table
CREATE INDEX IF NOT EXISTS idx_snapshot_policy_dataset ON snapshot_policies(dataset); 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 { if _, err := db.Exec(schema); err != nil {
return fmt.Errorf("create schema: %w", err) return fmt.Errorf("create schema: %w", err)

View File

@@ -26,7 +26,7 @@ type Config struct {
Addr string Addr string
TemplatesDir string TemplatesDir string
StaticDir 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 { type App struct {
@@ -103,8 +103,8 @@ func New(cfg Config) (*App, error) {
// Initialize database (optional) // Initialize database (optional)
var database *db.DB var database *db.DB
if cfg.DatabasePath != "" { if cfg.DatabaseConn != "" {
dbConn, err := db.New(cfg.DatabasePath) dbConn, err := db.New(cfg.DatabaseConn)
if err != nil { if err != nil {
return nil, fmt.Errorf("init database: %w", err) 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(), ISCSITargets: a.iscsiStore.List(),
Policies: a.snapshotPolicy.List(), Policies: a.snapshotPolicy.List(),
Config: map[string]interface{}{ 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 { if a.database != nil {
info.Database = DatabaseInfo{ info.Database = DatabaseInfo{
Connected: true, 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(), ISCSITargets: a.iscsiStore.List(),
Policies: a.snapshotPolicy.List(), Policies: a.snapshotPolicy.List(),
Config: map[string]interface{}{ Config: map[string]interface{}{
"database_path": a.cfg.DatabasePath, "database_conn": a.cfg.DatabaseConn,
}, },
} }

View File

@@ -28,7 +28,7 @@ func NewTestServer(t *testing.T) *TestServer {
Addr: ":0", // Use random port Addr: ":0", // Use random port
TemplatesDir: templatesDir, TemplatesDir: templatesDir,
StaticDir: staticDir, StaticDir: staticDir,
DatabasePath: "", // Empty = in-memory mode (no database) DatabaseConn: "", // Empty = in-memory mode (no database)
}) })
if err != nil { if err != nil {
t.Fatalf("create test app: %v", err) t.Fatalf("create test app: %v", err)