asdsd
This commit is contained in:
@@ -50,7 +50,12 @@ $modules = array_values(array_filter(
|
||||
<p>Module mit Login-Pflicht erscheinen erst nach passender Anmeldung.</p>
|
||||
</div>
|
||||
<?php if ($authUser !== null): ?>
|
||||
<a class="nav-link" href="/modules">Module verwalten</a>
|
||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<a class="nav-link" href="/modules">Module verwalten</a>
|
||||
<?php if (auth_is_admin()): ?>
|
||||
<a class="nav-link" href="/exports/database.sql">SQL-Export</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ $publicPaths = [
|
||||
'auth/me',
|
||||
'module/pi_control/terminal_info',
|
||||
];
|
||||
$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'debug'], true)
|
||||
$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'debug', 'exports/database.sql'], true)
|
||||
|| str_starts_with($uriPath, 'modules/setup/');
|
||||
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
|
||||
$user = auth_user();
|
||||
@@ -76,6 +76,23 @@ if ($uriPath === 'auth/me') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'exports/database.sql') {
|
||||
require_admin();
|
||||
$pdo = app()->basePdo() ?: app()->pdo();
|
||||
if (!$pdo instanceof PDO) {
|
||||
http_response_code(500);
|
||||
exit('Keine Datenbankverbindung fuer den Export verfuegbar.');
|
||||
}
|
||||
|
||||
$filename = 'nexus-export-' . gmdate('Ymd-His') . '.sql';
|
||||
$sql = (new \App\SqlDataExporter())->export($pdo, 'nexus');
|
||||
header('Content-Type: application/sql; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
echo $sql;
|
||||
exit;
|
||||
}
|
||||
|
||||
if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) {
|
||||
$moduleName = $moduleAuthMatches[1];
|
||||
$moduleMeta = app()->modules()->get($moduleName);
|
||||
|
||||
285
src/App/SqlDataExporter.php
Normal file
285
src/App/SqlDataExporter.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class SqlDataExporter
|
||||
{
|
||||
public function export(PDO $pdo, string $label = 'nexus'): string
|
||||
{
|
||||
$driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$tables = $this->orderedTables($pdo, $driver);
|
||||
$lines = [
|
||||
'-- Nexus SQL data export',
|
||||
'-- Generated at: ' . gmdate('c'),
|
||||
'-- Driver: ' . $driver,
|
||||
'-- Label: ' . $label,
|
||||
'-- Restore target: empty database with existing schema/migrations.',
|
||||
'',
|
||||
'BEGIN;',
|
||||
'',
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$columns = $this->columns($pdo, $driver, $table);
|
||||
if ($columns === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = '-- Table: ' . $table;
|
||||
$quotedTable = $this->quoteIdentifier($table, $driver);
|
||||
$quotedColumns = array_map(fn(string $column): string => $this->quoteIdentifier($column, $driver), $columns);
|
||||
$select = 'SELECT ' . implode(', ', $quotedColumns) . ' FROM ' . $quotedTable;
|
||||
|
||||
$count = 0;
|
||||
$stmt = $pdo->query($select);
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$values = [];
|
||||
foreach ($columns as $column) {
|
||||
$values[] = $this->quoteValue($pdo, $row[$column] ?? null);
|
||||
}
|
||||
|
||||
$lines[] = 'INSERT INTO ' . $quotedTable . ' (' . implode(', ', $quotedColumns) . ') VALUES (' . implode(', ', $values) . ');';
|
||||
$count++;
|
||||
}
|
||||
|
||||
$lines[] = '-- Rows: ' . $count;
|
||||
foreach ($this->sequenceSetvalLines($pdo, $driver, $table, $columns) as $sequenceLine) {
|
||||
$lines[] = $sequenceLine;
|
||||
}
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
$lines[] = 'COMMIT;';
|
||||
$lines[] = '';
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private function orderedTables(PDO $pdo, string $driver): array
|
||||
{
|
||||
$tables = $this->tables($pdo, $driver);
|
||||
$dependencies = $this->tableDependencies($pdo, $driver, $tables);
|
||||
$ordered = [];
|
||||
$remaining = array_fill_keys($tables, true);
|
||||
|
||||
while ($remaining !== []) {
|
||||
$progress = false;
|
||||
foreach (array_keys($remaining) as $table) {
|
||||
$parents = array_filter(
|
||||
$dependencies[$table] ?? [],
|
||||
static fn(string $parent): bool => isset($remaining[$parent])
|
||||
);
|
||||
if ($parents !== []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ordered[] = $table;
|
||||
unset($remaining[$table]);
|
||||
$progress = true;
|
||||
}
|
||||
|
||||
if (!$progress) {
|
||||
foreach (array_keys($remaining) as $table) {
|
||||
$ordered[] = $table;
|
||||
unset($remaining[$table]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered;
|
||||
}
|
||||
|
||||
private function tables(PDO $pdo, string $driver): array
|
||||
{
|
||||
if ($driver === 'pgsql') {
|
||||
$stmt = $pdo->query(
|
||||
"SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name"
|
||||
);
|
||||
return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN)));
|
||||
}
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
$stmt = $pdo->query(
|
||||
"SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name"
|
||||
);
|
||||
return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN)));
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
$stmt = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
||||
return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN)));
|
||||
}
|
||||
|
||||
throw new \RuntimeException('SQL export supports pgsql, mysql and sqlite only.');
|
||||
}
|
||||
|
||||
private function columns(PDO $pdo, string $driver, string $table): array
|
||||
{
|
||||
if ($driver === 'pgsql') {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = :table
|
||||
ORDER BY ordinal_position"
|
||||
);
|
||||
$stmt->execute(['table' => $table]);
|
||||
return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN)));
|
||||
}
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
ORDER BY ordinal_position"
|
||||
);
|
||||
$stmt->execute(['table' => $table]);
|
||||
return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN)));
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
$stmt = $pdo->query('PRAGMA table_info(' . $this->quoteIdentifier($table, $driver) . ')');
|
||||
$columns = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$columns[] = (string)$row['name'];
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function tableDependencies(PDO $pdo, string $driver, array $tables): array
|
||||
{
|
||||
$known = array_fill_keys($tables, true);
|
||||
$dependencies = [];
|
||||
foreach ($tables as $table) {
|
||||
$dependencies[$table] = [];
|
||||
}
|
||||
|
||||
if ($driver === 'pgsql') {
|
||||
$stmt = $pdo->query(
|
||||
"SELECT tc.table_name AS child_table, ccu.table_name AS parent_table
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.constraint_schema = tc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = current_schema()
|
||||
AND ccu.table_schema = current_schema()"
|
||||
);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$child = (string)$row['child_table'];
|
||||
$parent = (string)$row['parent_table'];
|
||||
if (isset($known[$child], $known[$parent]) && $child !== $parent) {
|
||||
$dependencies[$child][] = $parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
$stmt = $pdo->query(
|
||||
"SELECT table_name AS child_table, referenced_table_name AS parent_table
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_schema = DATABASE()
|
||||
AND referenced_table_name IS NOT NULL"
|
||||
);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$child = (string)$row['child_table'];
|
||||
$parent = (string)$row['parent_table'];
|
||||
if (isset($known[$child], $known[$parent]) && $child !== $parent) {
|
||||
$dependencies[$child][] = $parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
foreach ($tables as $table) {
|
||||
$stmt = $pdo->query('PRAGMA foreign_key_list(' . $this->quoteIdentifier($table, $driver) . ')');
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$parent = (string)($row['table'] ?? '');
|
||||
if (isset($known[$parent]) && $parent !== $table) {
|
||||
$dependencies[$table][] = $parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dependencies as $table => $parents) {
|
||||
$dependencies[$table] = array_values(array_unique($parents));
|
||||
}
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
private function quoteIdentifier(string $identifier, string $driver): string
|
||||
{
|
||||
$parts = explode('.', $identifier);
|
||||
$quote = $driver === 'mysql' ? '`' : '"';
|
||||
return implode('.', array_map(
|
||||
static fn(string $part): string => $quote . str_replace($quote, $quote . $quote, $part) . $quote,
|
||||
$parts
|
||||
));
|
||||
}
|
||||
|
||||
private function sequenceSetvalLines(PDO $pdo, string $driver, string $table, array $columns): array
|
||||
{
|
||||
if ($driver !== 'pgsql') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
foreach ($columns as $column) {
|
||||
$sequenceStmt = $pdo->prepare('SELECT pg_get_serial_sequence(:table, :column)');
|
||||
$sequenceStmt->execute(['table' => $table, 'column' => $column]);
|
||||
$sequence = (string)($sequenceStmt->fetchColumn() ?: '');
|
||||
if ($sequence === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$maxStmt = $pdo->query(
|
||||
'SELECT MAX(' . $this->quoteIdentifier($column, $driver) . ') FROM ' . $this->quoteIdentifier($table, $driver)
|
||||
);
|
||||
$max = $maxStmt->fetchColumn();
|
||||
$maxValue = is_numeric($max) ? (int)$max : 0;
|
||||
if ($maxValue > 0) {
|
||||
$lines[] = 'SELECT setval(' . $this->quoteValue($pdo, $sequence) . ', ' . $maxValue . ', true);';
|
||||
} else {
|
||||
$lines[] = 'SELECT setval(' . $this->quoteValue($pdo, $sequence) . ', 1, false);';
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function quoteValue(PDO $pdo, mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return is_finite((float)$value) ? (string)$value : 'NULL';
|
||||
}
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
}
|
||||
|
||||
return $pdo->quote((string)$value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user