This commit is contained in:
226
docs/POSTGRESQL_MIGRATION.md
Normal file
226
docs/POSTGRESQL_MIGRATION.md
Normal 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
3
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
|
||||
)
|
||||
|
||||
28
go.sum
28
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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user