====== edwiki-import.php ====== Spielt Import-Pakete ein, ohne dass gitbacked bei jeder Seite committet: gitbacked aus → Paket entpacken → struct-CSV einlesen → (nächstes Paket …) → Abschließen: Suchindex → gitbacked an → edwiki-git-sync.php. Ablage: neben doku.php im Wiki-Hauptordner. Aufruf: https:///edwiki-import.php (als Admin angemeldet). Herunterladen: auf den Dateinamen über dem Code klicken. ===== Paketformat (ZIP) ===== ^ Inhalt ^ Wirkung ^ | pages//.txt | wird nach data/pages/ geschrieben (überschreibt) | | media// | wird nach data/media/ geschrieben (überschreibt) | | _.csv | struct-Daten: Spalte pid + Felder des Schemas; schema.feld für weitere Schemas (z. B. relations.related). Mehrfachwerte mit Komma getrennt. Vorhandene Daten anderer Felder/Schemas bleiben erhalten (anders als beim struct-CSV-Import im Admin-Menü). | | alles andere | wird ignoriert (z. B. luecken_*.txt) | ===== Bedienung ===== - Paket hochladen (Formular) oder – wenn größer als die Upload-Grenze – per FTP nach data/tmp/edwiki-import/eingang/ legen und Seite neu laden; dann auswählen. - Beliebig viele Pakete nacheinander; gitbacked bleibt dazwischen aus (gelber Hinweis oben). - Zum Schluss einmal **Abschließen**: Suchindex für alle eingespielten Seiten, gitbacked an, Link zu edwiki-git-sync.php. - Läuft in Häppchen von 20 s (Seite lädt sich selbst neu). Bei Abbruch (Browser zu, 504): Seite neu öffnen → „Fortsetzen“. - Notfall: „Abbrechen und gitbacked wieder einschalten“. _*.csv → struct-Daten (Spalte pid + Felder; "schema.feld" für andere Schemas, * z. B. relations.related; Mehrfachwerte mit Komma) * * Ablauf: * „Paket einspielen“ (beliebig oft): gitbacked aus → entpacken → CSV einlesen * „Abschließen“ (einmal): Suchindex für alle neuen Seiten → gitbacked an → edwiki-git-sync.php * * Alles läuft in kurzen Etappen (Seite lädt sich selbst neu), damit lima-city nicht abbricht. * Anders als der CSV-Import im Admin-Menü bleiben andere Schemas der Seite (z. B. relations) erhalten. * * Benutzung: Datei neben doku.php legen, als Admin anmelden, https://…/edwiki-import.php aufrufen. * Pakete per Formular hochladen oder per FTP nach data/tmp/edwiki-import/eingang/ legen. */ if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/'); require_once DOKU_INC . 'inc/init.php'; use dokuwiki\plugin\struct\meta\Assignments; use dokuwiki\plugin\struct\meta\StructException; global $INPUT, $conf; header('Content-Type: text/html; charset=utf-8'); header('X-Robots-Tag: noindex'); const EDW_SELF = 'edwiki-import.php'; const EDW_SYNC = 'edwiki-git-sync.php'; const EDW_BUDGET = 20; // Sekunden Arbeit pro Aufruf const EDW_SUMMARY = 'Import-Paket'; $work = $conf['tmpdir'] . '/edwiki-import'; $inbox = $work . '/eingang'; $stateFile = $work . '/state.json'; io_mkdir_p($inbox); $t0 = microtime(true); // ------------------------------------------------------------------ Hilfen function edw_page($title, $body, $refresh = false) { $meta = $refresh ? '' : ''; echo '' . $meta . '' . hsc($title) . '' . '' . '

' . hsc($title) . '

' . $body . ''; exit; } function edw_state() { global $stateFile; $s = @json_decode((string)@file_get_contents($stateFile), true); return is_array($s) ? $s : ['step' => 'idle', 'packages' => [], 'index' => [], 'csv' => [], 'log' => [], 'gitbacked_off' => false]; } function edw_save($s) { global $stateFile; io_saveFile($stateFile, json_encode($s, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } function edw_log(&$s, $msg) { $s['log'][] = date('H:i:s') . ' ' . $msg; if (count($s['log']) > 400) $s['log'] = array_slice($s['log'], -400); } function edw_time_left() { global $t0; return microtime(true) - $t0 < EDW_BUDGET; } function edw_form($action, $label, $extra = '') { return '
' . '' . '' . $extra . '
'; } function edw_gitbacked_set($on) { if (!is_dir(DOKU_PLUGIN . 'gitbacked')) return true; // nicht installiert $ok = $on ? plugin_enable('gitbacked') : plugin_disable('gitbacked'); // PHP-Zwischenspeicher (OPcache) würde die alte Plugin-Einstellung sonst noch eine Weile verwenden if (function_exists('opcache_invalidate')) { foreach (glob(DOKU_CONF . 'plugins*.php') ?: [] as $f) @opcache_invalidate($f, true); } return $ok; } /** true, wenn gitbacked in DIESEM Aufruf noch aktiv ist (Handler geladen) */ function edw_gitbacked_active() { return class_exists('action_plugin_gitbacked_editcommit', false); } /** sicherer Zielpfad für einen ZIP-Eintrag, sonst null */ function edw_target($name, &$kind, &$id) { global $conf; $name = str_replace('\\', '/', $name); if ($name === '' || substr($name, -1) === '/' || strpos($name, '..') !== false || $name[0] === '/') return null; if (preg_match('#^pages/(.+)\.txt$#', $name, $m)) { $id = str_replace('/', ':', $m[1]); if (cleanID($id) !== $id) return null; $kind = 'page'; return wikiFN($id); } if (preg_match('#^media/(.+)$#', $name, $m)) { $id = str_replace('/', ':', $m[1]); if (cleanID($id) !== $id) return null; $kind = 'media'; return mediaFN($id); } if (preg_match('#^([a-z0-9_]+)_[^/]*\.csv$#', $name)) { $kind = 'csv'; $id = $name; return null; // wird gesondert behandelt } return null; } // ------------------------------------------------------------------ Zugriff if (!auth_isadmin()) { http_response_code(403); edw_page('Kein Zugriff', '

Bitte zuerst im Wiki als Administrator anmelden und die Seite neu laden.

'); } @set_time_limit(EDW_BUDGET + 30); $s = edw_state(); if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_POST) && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) { edw_page('Paket zu groß', '

Die Datei (' . round($_SERVER['CONTENT_LENGTH'] / 1048576, 1) . ' MB) ist größer als die Upload-Grenze des Servers (' . hsc(ini_get('post_max_size')) . ').

' . '

Bitte per FTP nach data/tmp/edwiki-import/eingang/ legen und diese Seite neu laden – ' . 'das Paket erscheint dann zur Auswahl.

Zurück

'); } $action = $INPUT->post->str('action'); $go = $INPUT->get->bool('go'); if (($action || $go) && !checkSecurityToken()) { edw_page('Fehler', '

Sicherheits-Token ungültig. Bitte Seite neu laden.

'); } // ------------------------------------------------------------------ Aktionen if ($action === 'start') { if ($s['step'] !== 'idle') edw_page('Läuft noch', '

Es läuft bereits ein Vorgang.

'); $zip = null; if (!empty($_FILES['paket']['tmp_name']) && is_uploaded_file($_FILES['paket']['tmp_name'])) { $name = preg_replace('/[^\w.-]/', '_', basename($_FILES['paket']['name'])); $zip = "$work/" . date('Ymd-His') . "-$name"; move_uploaded_file($_FILES['paket']['tmp_name'], $zip); } elseif ($INPUT->post->str('inbox') !== '') { $src = $inbox . '/' . basename($INPUT->post->str('inbox')); if (is_file($src)) { $zip = "$work/" . date('Ymd-His') . '-' . basename($src); rename($src, $zip); } } if (!$zip || !is_file($zip)) { edw_page('Fehler', '

Kein Paket erhalten. Ist die Datei größer als die Upload-Grenze? ' . 'Dann per FTP nach data/tmp/edwiki-import/eingang/ legen.

Zurück

'); } $s['packages'][] = basename($zip); $s['current'] = ['zip' => $zip, 'pos' => 0, 'pages' => 0, 'media' => 0]; $s['step'] = 'disable'; edw_log($s, 'Paket ' . basename($zip) . ' angenommen'); edw_save($s); $go = true; } if ($action === 'finish') { if ($s['step'] !== 'idle') edw_page('Läuft noch', '

Es läuft noch ein Vorgang.

'); $s['step'] = 'index'; $s['index_total'] = count($s['index']); edw_log($s, 'Abschluss gestartet: ' . count($s['index']) . ' Seiten indexieren'); edw_save($s); $go = true; } if ($action === 'abort') { edw_gitbacked_set(true); $old = $s; @unlink($stateFile); edw_page('Abgebrochen', '

gitbacked ist wieder an. Bereits eingespielte Seiten und Daten bleiben erhalten ' . '(Suchindex ggf. über das searchindex-Plugin neu aufbauen, dann ' . EDW_SYNC . ').

' . '
' . hsc(implode("\n", array_slice($old['log'], -40))) . '

Zurück

'); } // ------------------------------------------------------------------ Etappen if ($go) { $msg = ''; if ($s['step'] === 'disable') { // in diesem Aufruf sind die gitbacked-Handler noch geladen -> erst im nächsten Aufruf weiterarbeiten if (!$s['gitbacked_off']) { if (edw_gitbacked_set(false)) { $s['gitbacked_off'] = true; edw_log($s, 'gitbacked ausgeschaltet'); } else { edw_log($s, 'FEHLER: gitbacked konnte nicht ausgeschaltet werden'); $s['step'] = 'idle'; edw_save($s); edw_page('Fehler', '

gitbacked ließ sich nicht ausschalten (Schreibrechte auf conf/plugins.local.php?).

'); } } $s['step'] = 'extract'; edw_save($s); edw_page('Paket einspielen', '

gitbacked ist aus. Entpacken beginnt …

', true); } if (in_array($s['step'], ['extract', 'csv']) && edw_gitbacked_active()) { // Einstellung noch nicht wirksam: kurz warten, sonst würde jede Zeile einzeln committet $s['wait'] = ($s['wait'] ?? 0) + 1; if ($s['wait'] > 30) { $s['step'] = 'idle'; edw_log($s, 'FEHLER: gitbacked bleibt aktiv – Vorgang angehalten'); edw_save($s); edw_page('Fehler', '

gitbacked ist trotz Abschaltung noch aktiv. Bitte im Erweiterungsmanager ' . 'prüfen und dann „Fortsetzen“ über die Übersicht.

Zurück

'); } edw_save($s); edw_page('Paket einspielen', '

Warte, bis die Abschaltung von gitbacked wirksam ist … (' . $s['wait'] . ')

', true); } unset($s['wait']); if ($s['step'] === 'extract') { $c = &$s['current']; $za = new ZipArchive(); if ($za->open($c['zip']) !== true) { edw_log($s, 'FEHLER: ZIP lässt sich nicht öffnen'); $s['step'] = 'idle'; edw_save($s); edw_page('Fehler', '

Die ZIP-Datei lässt sich nicht öffnen.

Zurück

'); } $n = $za->numFiles; while ($c['pos'] < $n && edw_time_left()) { $name = $za->getNameIndex($c['pos']); $c['pos']++; $kind = $id = null; $target = edw_target($name, $kind, $id); if ($kind === 'csv') { $dst = $work . '/' . date('Ymd-His') . '-' . basename($name); file_put_contents($dst, $za->getFromIndex($c['pos'] - 1)); $s['csv'][] = ['file' => $dst, 'schema' => explode('_', basename($name))[0], 'line' => 0]; edw_log($s, 'CSV gefunden: ' . basename($name)); continue; } if (!$target) continue; io_makeFileDir($target); if (file_put_contents($target, $za->getFromIndex($c['pos'] - 1)) === false) { edw_log($s, "FEHLER beim Schreiben: $name"); continue; } if ($kind === 'page') { $s['index'][$id] = 1; $c['pages']++; } else { $c['media']++; } } $za->close(); if ($c['pos'] >= $n) { edw_log($s, "Entpackt: {$c['pages']} Seiten, {$c['media']} Medien"); $s['step'] = 'csv'; } edw_save($s); edw_page('Paket einspielen', "

Entpacken: {$c['pos']} von $n Einträgen …

", true); } if ($s['step'] === 'csv') { /** @var helper_plugin_struct $helper */ $helper = plugin_load('helper', 'struct'); $assign = Assignments::getInstance(); $done = true; foreach ($s['csv'] as &$job) { if (!empty($job['done'])) continue; $fh = fopen($job['file'], 'rb'); $header = fgetcsv($fh); for ($i = 0; $i < $job['line']; $i++) fgetcsv($fh); $job['total'] = $job['total'] ?? max(0, count(file($job['file'])) - 1); $row = null; while (edw_time_left() && ($row = fgetcsv($fh)) !== false) { $job['line']++; if (count($row) !== count($header)) { edw_log($s, "Zeile {$job['line']}: falsche Spaltenzahl, übersprungen"); continue; } $rec = array_combine($header, $row); $pid = cleanID($rec['pid'] ?? ''); unset($rec['pid']); if (!$pid || !page_exists($pid)) { edw_log($s, "Zeile {$job['line']}: Seite „{$pid}“ fehlt, übersprungen"); continue; } try { $cols = []; foreach ($rec as $col => $val) { [$schema, $field] = strpos($col, '.') ? explode('.', $col, 2) : [$job['schema'], $col]; $cols[] = [$schema, $field, $val]; $assign->assignPageSchema($pid, $schema); } $data = $helper->getData($pid); // vorhandene Daten aller Schemas behalten foreach ($cols as [$schema, $field, $val]) { static $schemaCache = []; $schemaCache[$schema] ??= helper_plugin_struct::getSchema($schema)[$schema]; $column = $schemaCache[$schema]->findColumn($field); if (!$column) { edw_log($s, "Spalte $schema.$field gibt es nicht – ignoriert"); continue; } $data[$schema][$field] = $column->isMulti() ? array_values(array_filter(array_map('trim', explode(',', $val)), 'strlen')) : $val; } $helper->saveData($pid, $data, EDW_SUMMARY, true); } catch (StructException $e) { edw_log($s, "Zeile {$job['line']} ($pid): " . $e->getMessage()); } } $eof = feof($fh) || $row === false; fclose($fh); if ($eof) { $job['done'] = true; edw_log($s, 'CSV fertig: ' . basename($job['file']) . " ({$job['line']} Zeilen)"); } else { $done = false; $msg = 'CSV ' . hsc(basename($job['file'])) . ": {$job['line']} von {$job['total']} Zeilen …" . ""; break; } } unset($job); if ($done) { $s['step'] = 'idle'; unset($s['current']); edw_save($s); header('Location: ' . EDW_SELF); exit; } edw_save($s); edw_page('Paket einspielen', "

$msg

", true); } if ($s['step'] === 'index') { $indexer = class_exists('\\dokuwiki\\Search\\Indexer') ? new \dokuwiki\Search\Indexer() : null; if (!$indexer && is_file(DOKU_INC . 'inc/indexer.php')) require_once DOKU_INC . 'inc/indexer.php'; $ids = array_keys($s['index']); foreach ($ids as $id) { if (!edw_time_left()) break; if (page_exists($id)) { try { p_get_metadata($id, '', METADATA_RENDER_UNLIMITED); // Titel, Bilder usw. $indexer ? $indexer->addPage($id, true) : idx_addPage($id, false, true); } catch (\Throwable $e) { edw_log($s, "Index $id: " . $e->getMessage()); } } unset($s['index'][$id]); } $left = count($s['index']); $total = $s['index_total'] ?: 1; if ($left) { edw_save($s); edw_page('Abschließen', '

Suchindex: ' . ($total - $left) . " von $total Seiten …

" . '', true); } edw_gitbacked_set(true); edw_log($s, "Suchindex fertig ($total Seiten), gitbacked wieder an"); $log = $s['log']; @unlink($stateFile); foreach (glob("$work/*") ?: [] as $f) if (is_file($f)) @unlink($f); edw_page('Fertig', '

Alles eingespielt und indexiert. gitbacked ist wieder an.

' . '

' . '
' . hsc(implode("\n", array_slice($log, -60))) . '
'); } } // ------------------------------------------------------------------ Übersicht $body = ''; if ($s['gitbacked_off']) { $body .= '

gitbacked ist AUS – ' . count($s['packages']) . ' Paket(e) eingespielt, ' . count($s['index']) . ' Seiten warten auf den Suchindex. Zum Schluss „Abschließen“ klicken.

'; } if ($s['step'] !== 'idle') { $body .= '

Ein Vorgang wurde unterbrochen (Schritt: ' . hsc($s['step']) . ').

' . '

'; } $limit = ini_get('upload_max_filesize') . ' (POST: ' . ini_get('post_max_size') . ')'; $files = glob("$inbox/*.zip") ?: []; $inboxSel = ''; foreach ($files as $f) { $inboxSel .= '
'; } if ($s['step'] === 'idle') { $body .= '
1. Paket einspielen (beliebig oft)' . edw_form('start', 'Paket einspielen', '

ZIP hochladen (Grenze: ' . hsc($limit) . '):

' . ($inboxSel ? '

… oder aus data/tmp/edwiki-import/eingang/:
' . $inboxSel . '

' : '

Größere Pakete per FTP nach data/tmp/edwiki-import/eingang/ legen und Seite neu laden.

')) . '
'; $body .= '
2. Abschließen (einmal, am Ende)' . '

Suchindex für ' . count($s['index']) . ' Seiten → gitbacked an → weiter zu ' . EDW_SYNC . '

' . edw_form('finish', 'Abschließen') . '
'; } if ($s['gitbacked_off'] || $s['step'] !== 'idle') { $body .= '

Notfall: ' . edw_form('abort', 'Abbrechen und gitbacked wieder einschalten') . '

'; } if ($s['log']) $body .= '

Protokoll

' . hsc(implode("\n", array_slice($s['log'], -40))) . '
'; edw_page('Import-Pakete einspielen', $body);