Migrate panel DB from MySQL to SQLite

Panel no longer depends on the user-managed MariaDB service.
SQLite at /var/lib/novacpx/panel.db runs independently so the
control panel stays up even when MariaDB is stopped.

- DB.php: switch to sqlite: DSN, add SQL translator (ON DUPLICATE KEY,
  DATE_ADD/DATE_SUB INTERVAL, NOW(), UNIX_TIMESTAMP(), IFNULL)
- Core.php: replace DB_HOST/NAME/USER/PASS with DB_PATH constant
- schema.sql: full SQLite syntax, add TOTP columns to users table
- _branding.php: use sqlite: PDO, datetime('now') for session check
- install.sh: apt install sqlite3, create SQLite DB instead of MySQL DB
- tools/migrate-to-sqlite.sh: one-shot migration script for existing installs
This commit is contained in:
2026-06-09 14:52:02 +00:00
parent 9bd78a81ea
commit fbc445dad2
5 changed files with 713 additions and 335 deletions
+3 -4
View File
@@ -13,10 +13,9 @@ if (!$_cfg) {
die(json_encode(['error' => 'NovaCPX not configured. Run the installer.']));
}
define('DB_HOST', $_cfg['database']['host'] ?? 'localhost');
define('DB_NAME', $_cfg['database']['name'] ?? 'novacpx');
define('DB_USER', $_cfg['database']['user'] ?? '');
define('DB_PASS', $_cfg['database']['pass'] ?? '');
define('DB_PATH', $_cfg['database']['path'] ?? '/var/lib/novacpx/panel.db');
define('DB_WP_USER', $_cfg['database']['wp_user'] ?? '');
define('DB_WP_PASS', $_cfg['database']['wp_pass'] ?? '');
define('SECRET_KEY', $_cfg['panel']['secret'] ?? '');
define('PANEL_VER', $_cfg['panel']['version'] ?? NOVACPX_VERSION);
define('PORT_USER', (int)($_cfg['panel']['port_user'] ?? 8880));
+82 -4
View File
@@ -4,15 +4,21 @@ class DB {
private PDO $pdo;
private function __construct() {
$path = defined('DB_PATH') ? DB_PATH : '/var/lib/novacpx/panel.db';
$dir = dirname($path);
if (!is_dir($dir)) mkdir($dir, 0750, true);
$this->pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER, DB_PASS,
"sqlite:{$path}",
null, null,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
$this->pdo->exec("PRAGMA journal_mode = WAL");
$this->pdo->exec("PRAGMA foreign_keys = ON");
$this->pdo->exec("PRAGMA busy_timeout = 5000");
}
public static function getInstance(): self {
@@ -20,8 +26,80 @@ class DB {
return self::$instance;
}
// Translate MySQL-isms to SQLite equivalents
private function translate(string $sql): string {
// ON DUPLICATE KEY UPDATE col=VALUES(col) → ON CONFLICT DO UPDATE SET col=excluded.col
$sql = preg_replace_callback(
'/ON DUPLICATE KEY UPDATE\s+(.+?)(?=\s*(?:;|$))/is',
function (array $m): string {
$pairs = preg_split('/,\s*/', trim($m[1]));
$sets = array_map(function (string $pair): string {
if (preg_match('/(\w+)\s*=\s*VALUES\s*\(\s*(\w+)\s*\)/i', $pair, $pm)) {
return "{$pm[1]}=excluded.{$pm[2]}";
}
// col=? or col=expr — keep as-is
return $pair;
}, $pairs);
return 'ON CONFLICT DO UPDATE SET ' . implode(', ', $sets);
},
$sql
);
// NOW() → datetime('now')
$sql = preg_replace('/\bNOW\(\)/i', "datetime('now')", $sql);
// UNIX_TIMESTAMP() → strftime('%s','now')
$sql = preg_replace('/\bUNIX_TIMESTAMP\(\)/i', "strftime('%s','now')", $sql);
// DATE_ADD(expr, INTERVAL n UNIT) → datetime(expr, '+n unit')
$sql = preg_replace_callback(
"/DATE_ADD\s*\(\s*(NOW\(\)|datetime\('now'\))\s*,\s*INTERVAL\s+(\d+)\s+(\w+)\s*\)/i",
function (array $m): string {
$n = $m[2];
$unit = strtolower($m[3]);
// Map MySQL interval units to SQLite modifier strings
$map = [
'second' => 'second', 'seconds' => 'second',
'minute' => 'minute', 'minutes' => 'minute',
'hour' => 'hour', 'hours' => 'hour',
'day' => 'day', 'days' => 'day',
'month' => 'month', 'months' => 'month',
'year' => 'year', 'years' => 'year',
];
$mod = $map[$unit] ?? $unit;
return "datetime('now', '+{$n} {$mod}')";
},
$sql
);
// DATE_SUB(expr, INTERVAL n UNIT) → datetime(expr, '-n unit')
$sql = preg_replace_callback(
"/DATE_SUB\s*\(\s*(NOW\(\)|datetime\('now'\))\s*,\s*INTERVAL\s+(\d+)\s+(\w+)\s*\)/i",
function (array $m): string {
$n = $m[2];
$unit = strtolower($m[3]);
$map = [
'second' => 'second', 'seconds' => 'second',
'minute' => 'minute', 'minutes' => 'minute',
'hour' => 'hour', 'hours' => 'hour',
'day' => 'day', 'days' => 'day',
'month' => 'month', 'months' => 'month',
'year' => 'year', 'years' => 'year',
];
$mod = $map[$unit] ?? $unit;
return "datetime('now', '-{$n} {$mod}')";
},
$sql
);
// IFNULL → COALESCE (SQLite supports both but be safe)
$sql = preg_replace('/\bIFNULL\s*\(/i', 'COALESCE(', $sql);
return $sql;
}
public function execute(string $sql, array $params = []): PDOStatement {
$stmt = $this->pdo->prepare($sql);
$stmt = $this->pdo->prepare($this->translate($sql));
$stmt->execute($params);
return $stmt;
}