importer 2
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-11 03:09:09 +02:00
parent 7400caa687
commit 35a0b10d0c
3 changed files with 214 additions and 3 deletions

View File

@@ -549,6 +549,7 @@
const [debugEntries, setDebugEntries] = useState([]); const [debugEntries, setDebugEntries] = useState([]);
const [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null)); const [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null));
const [initForm, setInitForm] = useState({ drop_existing: false }); const [initForm, setInitForm] = useState({ drop_existing: false });
const [sqlImportFile, setSqlImportFile] = useState(null);
const [dbCheck, setDbCheck] = useState(null); const [dbCheck, setDbCheck] = useState(null);
const [measurementForm, setMeasurementForm] = useState({ const [measurementForm, setMeasurementForm] = useState({
measured_at: '', 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() { async function testDatabaseConnection() {
setSaving(true); setSaving(true);
setError(''); setError('');
@@ -2866,6 +2899,27 @@
onClick: importOldData, onClick: importOldData,
disabled: saving, disabled: saving,
}, saving ? 'Importiert …' : 'Alte Daten importieren'), }, 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.', [ panel('Datenbank-Test', 'Prueft, ob das Modul die Projekt-Datenbank erreichen und eine einfache Anfrage ausfuehren kann.', [
dbCheck dbCheck

View File

@@ -103,6 +103,16 @@ final class Router
], 201); ], 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') { if ($resource === 'upgrade' && $method === 'POST') {
$this->respond([ $this->respond([
'data' => $this->schemaManager()->upgradeSchemaDirect(), 'data' => $this->schemaManager()->upgradeSchemaDirect(),

View File

@@ -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 private function tableExists(string $table): bool
{ {
return in_array($table, $this->existingTables([$table]), true); return in_array($table, $this->existingTables([$table]), true);
@@ -378,7 +413,14 @@ final class SchemaManager
} }
$sql = (string) file_get_contents($schemaFile); $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<string> $statements
*/
private function executeSqlStatements(array $statements, string $sourceLabel, string $errorMessage = 'SQL-Import fuer Mining-Checker fehlgeschlagen.'): int
{
$currentStatement = null; $currentStatement = null;
$useTransaction = $this->driver === 'pgsql' && !$this->pdo->inTransaction(); $useTransaction = $this->driver === 'pgsql' && !$this->pdo->inTransaction();
@@ -387,35 +429,140 @@ final class SchemaManager
$this->pdo->beginTransaction(); $this->pdo->beginTransaction();
} }
$executed = 0;
foreach ($statements as $statement) { foreach ($statements as $statement) {
$trimmed = trim($statement); $trimmed = trim($statement);
if ($trimmed === '') { if ($trimmed === '') {
continue; continue;
} }
$currentStatement = $trimmed; $currentStatement = $trimmed;
$this->pdo->exec($trimmed); $this->pdo->exec($trimmed);
$executed++;
} }
if ($useTransaction && $this->pdo->inTransaction()) { if ($useTransaction && $this->pdo->inTransaction()) {
$this->pdo->commit(); $this->pdo->commit();
} }
return $executed;
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
if ($useTransaction && $this->pdo->inTransaction()) { if ($useTransaction && $this->pdo->inTransaction()) {
$this->pdo->rollBack(); $this->pdo->rollBack();
} }
throw new ApiException( throw new ApiException(
'Schema-Import fuer Mining-Checker fehlgeschlagen.', $errorMessage,
500, 500,
[ [
'message' => $exception->getMessage(), 'message' => $exception->getMessage(),
'schema_file' => $schemaFile, 'source' => $sourceLabel,
'statement' => $currentStatement !== null ? substr($currentStatement, 0, 1000) : null, 'statement' => $currentStatement !== null ? substr($currentStatement, 0, 1000) : null,
] ]
); );
} }
} }
/**
* @return list<string>
*/
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 private function dropExistingTables(): array
{ {
$tables = array_reverse($this->schemaStatus()['present_tables']); $tables = array_reverse($this->schemaStatus()['present_tables']);