From 35a0b10d0cb6463aee1cb09dcc3d1db20aca97f1 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 11 Apr 2026 03:09:09 +0200 Subject: [PATCH] importer 2 --- modules/mining-checker/assets/js/app.js | 54 +++++++ modules/mining-checker/src/Api/Router.php | 10 ++ .../src/Infrastructure/SchemaManager.php | 153 +++++++++++++++++- 3 files changed, 214 insertions(+), 3 deletions(-) diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index f714b27..09e98c8 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -549,6 +549,7 @@ const [debugEntries, setDebugEntries] = useState([]); const [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null)); const [initForm, setInitForm] = useState({ drop_existing: false }); + const [sqlImportFile, setSqlImportFile] = useState(null); const [dbCheck, setDbCheck] = useState(null); const [measurementForm, setMeasurementForm] = useState({ measured_at: '', @@ -1572,6 +1573,38 @@ } } + async function importSqlFile() { + if (!sqlImportFile) { + setError('Bitte zuerst eine SQL-Datei auswaehlen.'); + setMessage(''); + return; + } + + setSaving(true); + setError(''); + setMessage(''); + try { + const body = new FormData(); + body.append('sql_file', sqlImportFile); + const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/sql-import`, { + method: 'POST', + body, + timeoutMs: 30000, + }); + setSqlImportFile(null); + setMessage( + `${result.message || 'SQL-Datei wurde importiert.'} ` + + `${result.statement_count || 0} Statements aus ${result.file || 'der Datei'} ausgefuehrt.` + ); + await loadSchemaStatus(projectKey); + await loadBootstrap(projectKey); + } catch (err) { + setError(err.message); + } finally { + setSaving(false); + } + } + async function testDatabaseConnection() { setSaving(true); setError(''); @@ -2866,6 +2899,27 @@ onClick: importOldData, disabled: saving, }, saving ? 'Importiert …' : 'Alte Daten importieren'), + h('div', { key: 'sql-import', className: 'mc-form' }, [ + h('label', { className: 'mc-field' }, [ + h('span', { className: 'mc-field-label' }, 'SQL-Datei importieren'), + h('input', { + type: 'file', + accept: '.sql,text/sql,application/sql', + onChange: (event) => setSqlImportFile(event.target.files && event.target.files[0] ? event.target.files[0] : null), + }), + ]), + h('div', { className: 'mc-text' }, + sqlImportFile + ? `Ausgewaehlt: ${sqlImportFile.name}` + : 'Fuehrt die ausgewaehlte SQL-Datei direkt in der aktuellen Projekt-Datenbank aus. Bestehende Daten werden dabei nicht automatisch geloescht.' + ), + h('button', { + type: 'button', + className: 'mc-button mc-button--secondary', + onClick: importSqlFile, + disabled: saving || !sqlImportFile, + }, saving ? 'Importiert …' : 'SQL-Datei einspielen'), + ]), ]), panel('Datenbank-Test', 'Prueft, ob das Modul die Projekt-Datenbank erreichen und eine einfache Anfrage ausfuehren kann.', [ dbCheck diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index c70a02c..7449bee 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -103,6 +103,16 @@ final class Router ], 201); } + if ($resource === 'sql-import' && $method === 'POST') { + if (!isset($_FILES['sql_file']) || !is_array($_FILES['sql_file'])) { + throw new ApiException('Feld sql_file fehlt.', 422); + } + + $this->respond([ + 'data' => $this->schemaManager()->importSqlFile($_FILES['sql_file']), + ], 201); + } + if ($resource === 'upgrade' && $method === 'POST') { $this->respond([ 'data' => $this->schemaManager()->upgradeSchemaDirect(), diff --git a/modules/mining-checker/src/Infrastructure/SchemaManager.php b/modules/mining-checker/src/Infrastructure/SchemaManager.php index 25f39df..54b8291 100644 --- a/modules/mining-checker/src/Infrastructure/SchemaManager.php +++ b/modules/mining-checker/src/Infrastructure/SchemaManager.php @@ -365,6 +365,41 @@ final class SchemaManager ]; } + public function importSqlFile(array $uploadedFile): array + { + $errorCode = (int) ($uploadedFile['error'] ?? UPLOAD_ERR_NO_FILE); + if ($errorCode !== UPLOAD_ERR_OK) { + throw new ApiException( + 'SQL-Datei konnte nicht hochgeladen werden.', + 422, + ['upload_error' => $errorCode] + ); + } + + $originalName = (string) ($uploadedFile['name'] ?? 'import.sql'); + $tmpPath = (string) ($uploadedFile['tmp_name'] ?? ''); + if ($tmpPath === '' || !is_uploaded_file($tmpPath)) { + throw new ApiException('Ungueltige Upload-Datei fuer SQL-Import.', 422); + } + + if (!preg_match('/\.sql$/i', $originalName)) { + throw new ApiException('Bitte eine SQL-Datei mit Endung .sql hochladen.', 422, ['file' => $originalName]); + } + + $sql = @file_get_contents($tmpPath); + if (!is_string($sql) || trim($sql) === '') { + throw new ApiException('Die hochgeladene SQL-Datei ist leer oder konnte nicht gelesen werden.', 422, ['file' => $originalName]); + } + + $statementCount = $this->executeSqlStatements($this->splitSqlStatements($sql), $originalName); + + return [ + 'file' => $originalName, + 'statement_count' => $statementCount, + 'message' => 'SQL-Datei wurde erfolgreich eingespielt.', + ]; + } + private function tableExists(string $table): bool { return in_array($table, $this->existingTables([$table]), true); @@ -378,7 +413,14 @@ final class SchemaManager } $sql = (string) file_get_contents($schemaFile); - $statements = preg_split('/;\s*(?:\R|$)/', $sql) ?: []; + $this->executeSqlStatements($this->splitSqlStatements($sql), $schemaFile, 'Schema-Import fuer Mining-Checker fehlgeschlagen.'); + } + + /** + * @param list $statements + */ + private function executeSqlStatements(array $statements, string $sourceLabel, string $errorMessage = 'SQL-Import fuer Mining-Checker fehlgeschlagen.'): int + { $currentStatement = null; $useTransaction = $this->driver === 'pgsql' && !$this->pdo->inTransaction(); @@ -387,35 +429,140 @@ final class SchemaManager $this->pdo->beginTransaction(); } + $executed = 0; foreach ($statements as $statement) { $trimmed = trim($statement); if ($trimmed === '') { continue; } + $currentStatement = $trimmed; $this->pdo->exec($trimmed); + $executed++; } if ($useTransaction && $this->pdo->inTransaction()) { $this->pdo->commit(); } + + return $executed; } catch (\Throwable $exception) { if ($useTransaction && $this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw new ApiException( - 'Schema-Import fuer Mining-Checker fehlgeschlagen.', + $errorMessage, 500, [ 'message' => $exception->getMessage(), - 'schema_file' => $schemaFile, + 'source' => $sourceLabel, 'statement' => $currentStatement !== null ? substr($currentStatement, 0, 1000) : null, ] ); } } + /** + * @return list + */ + private function splitSqlStatements(string $sql): array + { + $statements = []; + $buffer = ''; + $length = strlen($sql); + $inSingleQuote = false; + $inDoubleQuote = false; + $inBacktickQuote = false; + $inLineComment = false; + $inBlockComment = false; + + for ($index = 0; $index < $length; $index++) { + $char = $sql[$index]; + $next = $index + 1 < $length ? $sql[$index + 1] : ''; + + if ($inLineComment) { + if ($char === "\n") { + $inLineComment = false; + $buffer .= $char; + } + continue; + } + + if ($inBlockComment) { + if ($char === '*' && $next === '/') { + $inBlockComment = false; + $index++; + } + continue; + } + + if (!$inSingleQuote && !$inDoubleQuote && !$inBacktickQuote) { + if ($char === '-' && $next === '-') { + $afterNext = $index + 2 < $length ? $sql[$index + 2] : ''; + if ($afterNext === '' || ctype_space($afterNext)) { + $inLineComment = true; + $index++; + continue; + } + } + + if ($char === '#') { + $inLineComment = true; + continue; + } + + if ($char === '/' && $next === '*') { + $inBlockComment = true; + $index++; + continue; + } + } + + if ($char === "'" && !$inDoubleQuote && !$inBacktickQuote) { + $escaped = $index > 0 && $sql[$index - 1] === '\\'; + if (!$escaped) { + $inSingleQuote = !$inSingleQuote; + } + $buffer .= $char; + continue; + } + + if ($char === '"' && !$inSingleQuote && !$inBacktickQuote) { + $escaped = $index > 0 && $sql[$index - 1] === '\\'; + if (!$escaped) { + $inDoubleQuote = !$inDoubleQuote; + } + $buffer .= $char; + continue; + } + + if ($char === '`' && !$inSingleQuote && !$inDoubleQuote) { + $inBacktickQuote = !$inBacktickQuote; + $buffer .= $char; + continue; + } + + if ($char === ';' && !$inSingleQuote && !$inDoubleQuote && !$inBacktickQuote) { + $trimmed = trim($buffer); + if ($trimmed !== '') { + $statements[] = $trimmed; + } + $buffer = ''; + continue; + } + + $buffer .= $char; + } + + $trimmed = trim($buffer); + if ($trimmed !== '') { + $statements[] = $trimmed; + } + + return $statements; + } + private function dropExistingTables(): array { $tables = array_reverse($this->schemaStatus()['present_tables']);