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://<wiki>/edwiki-import.php (als Admin angemeldet). Herunterladen: auf den Dateinamen über dem Code klicken.
Paketformat (ZIP)
| Inhalt | Wirkung |
|---|---|
| pages/<ns>/<seite>.txt | wird nach data/pages/ geschrieben (überschreibt) |
| media/<ns>/<datei> | wird nach data/media/ geschrieben (überschreibt) |
| <schema>_<name>.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“.
- edwiki-import.php
<?php /** * edwiki – Import-Pakete einspielen (Seiten, Medien, struct-Daten) ohne gitbacked-Ärger * * Ein Paket ist eine ZIP-Datei mit * pages/… → data/pages/… (Wiki-Seiten, z. B. pages/news/galnet/2023/01/02_01.txt) * media/… → data/media/… (Bilder) * <schema>_*.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 ? '<meta http-equiv="refresh" content="1;url=' . hsc(EDW_SELF . '?go=1§ok=' . getSecurityToken()) . '">' : ''; echo '<!doctype html><html lang="de"><head><meta charset="utf-8">' . $meta . '<title>' . hsc($title) . '</title>' . '<style>body{font-family:sans-serif;max-width:60em;margin:2em auto;padding:0 1em}' . 'pre{background:#f4f4f4;padding:1em;overflow:auto;max-height:25em}' . '.ok{color:#060}.err{color:#a00}.warn{background:#fff3cd;border:1px solid #e0c060;padding:.6em 1em}' . 'button{font-size:1.1em;padding:.4em 1.2em;margin:.2em 0}fieldset{margin:1em 0}' . 'progress{width:100%;height:1.4em}</style>' . '</head><body><h1>' . hsc($title) . '</h1>' . $body . '</body></html>'; 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 '<form method="post" enctype="multipart/form-data">' . '<input type="hidden" name="sectok" value="' . hsc(getSecurityToken()) . '">' . '<input type="hidden" name="action" value="' . hsc($action) . '">' . $extra . '<button type="submit">' . hsc($label) . '</button></form>'; } 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', '<p class="err">Bitte zuerst im Wiki als Administrator anmelden und die Seite neu laden.</p>'); } @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ß', '<p class="err">Die Datei (' . round($_SERVER['CONTENT_LENGTH'] / 1048576, 1) . ' MB) ist größer als die Upload-Grenze des Servers (' . hsc(ini_get('post_max_size')) . ').</p>' . '<p>Bitte per FTP nach <code>data/tmp/edwiki-import/eingang/
legen und diese Seite neu laden – '
. 'das Paket erscheint dann zur Auswahl.</p><p><a href="' . EDW_SELF . '">Zurück</a></p>');
} $action = $INPUT→post→str('action'); $go = $INPUT→get→bool('go'); if 1) {
edw_page('Fehler', '<p class="err">Sicherheits-Token ungültig. Bitte Seite neu laden.</p>');
}
// —————————————————————— Aktionen
if ($action === 'start') {
if ($s['step'] !== 'idle') edw_page('Läuft noch', '<p class="err">Es läuft bereits ein Vorgang.</p>');
$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', '<p class="err">Kein Paket erhalten. Ist die Datei größer als die Upload-Grenze? '
. 'Dann per FTP nach <code>data/tmp/edwiki-import/eingang/</code> legen.</p><p><a href="' . EDW_SELF . '">Zurück</a></p>');
}
$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', '<p class="err">Es läuft noch ein Vorgang.</p>');
$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', '<p>gitbacked ist wieder <strong>an</strong>. Bereits eingespielte Seiten und Daten bleiben erhalten '
. '(Suchindex ggf. über das searchindex-Plugin neu aufbauen, dann ' . EDW_SYNC . ').</p>'
. '<pre>' . hsc(implode("\n", array_slice($old['log'], -40))) . '</pre><p><a href="' . EDW_SELF . '">Zurück</a></p>');
}
// —————————————————————— 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', '<p class="err">gitbacked ließ sich nicht ausschalten (Schreibrechte auf conf/plugins.local.php?).</p>');
}
}
$s['step'] = 'extract';
edw_save($s);
edw_page('Paket einspielen', '<p>gitbacked ist aus. Entpacken beginnt …</p>', 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', '<p class="err">gitbacked ist trotz Abschaltung noch aktiv. Bitte im Erweiterungsmanager '
. 'prüfen und dann „Fortsetzen“ über die Übersicht.</p><p><a href="' . EDW_SELF . '">Zurück</a></p>');
}
edw_save($s);
edw_page('Paket einspielen', '<p>Warte, bis die Abschaltung von gitbacked wirksam ist … (' . $s['wait'] . ')</p>', 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', '<p class="err">Die ZIP-Datei lässt sich nicht öffnen.</p><p><a href="' . EDW_SELF . '">Zurück</a></p>');
}
$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', "<p>Entpacken: {$c['pos']} von $n Einträgen …</p><progress max=\"$n\" value=\"{$c['pos']}\"></progress>", 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 …"
. "<progress max=\"{$job['total']}\" value=\"{$job['line']}\"></progress>";
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', "<p>$msg</p>", 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', '<p>Suchindex: ' . ($total - $left) . " von $total Seiten …</p>"
. '<progress max="' . $total . '" value="' . ($total - $left) . '"></progress>', 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', '<p class="ok">Alles eingespielt und indexiert. gitbacked ist wieder <strong>an</strong>.</p>'
. '<p><a href="' . EDW_SYNC . '"><button>Weiter zu ' . EDW_SYNC . '</button></a></p>'
. '<pre>' . hsc(implode("\n", array_slice($log, -60))) . '</pre>');
}
}
// —————————————————————— Übersicht
$body = ''; if ($s['gitbacked_off']) {
$body .= '<p class="warn"><strong>gitbacked ist AUS</strong> – ' . count($s['packages']) . ' Paket(e) eingespielt, '
. count($s['index']) . ' Seiten warten auf den Suchindex. Zum Schluss „Abschließen“ klicken.</p>';
} if ($s['step'] !== 'idle') {
$body .= '<p class="err">Ein Vorgang wurde unterbrochen (Schritt: ' . hsc($s['step']) . ').</p>'
. '<p><a href="' . hsc(EDW_SELF . '?go=1§ok=' . getSecurityToken()) . '"><button>Fortsetzen</button></a></p>';
}
$limit = ini_get('upload_max_filesize') . ' (POST: ' . ini_get('post_max_size') . ')'; $files = glob(„$inbox/*.zip“) ?: []; $inboxSel = ''; foreach ($files as $f) {
$inboxSel .= '<label><input type="radio" name="inbox" value="' . hsc(basename($f)) . '"> ' . hsc(basename($f))
. ' (' . round(filesize($f) / 1048576, 1) . ' MB)</label><br>';
}
if ($s['step'] === 'idle') {
$body .= '<fieldset><legend>1. Paket einspielen (beliebig oft)</legend>'
. edw_form('start', 'Paket einspielen',
'<p>ZIP hochladen (Grenze: ' . hsc($limit) . '): <input type="file" name="paket" accept=".zip"></p>'
. ($inboxSel ? '<p>… oder aus <code>data/tmp/edwiki-import/eingang/</code>:<br>' . $inboxSel . '</p>'
: '<p><small>Größere Pakete per FTP nach <code>data/tmp/edwiki-import/eingang/</code> legen und Seite neu laden.</small></p>'))
. '</fieldset>';
$body .= '<fieldset><legend>2. Abschließen (einmal, am Ende)</legend>'
. '<p>Suchindex für ' . count($s['index']) . ' Seiten → gitbacked an → weiter zu ' . EDW_SYNC . '</p>'
. edw_form('finish', 'Abschließen') . '</fieldset>';
} if ($s['gitbacked_off'] || $s['step'] !== 'idle') {
$body .= '<p><small>Notfall: ' . edw_form('abort', 'Abbrechen und gitbacked wieder einschalten') . '</small></p>';
} if ($s['log']) $body .= '<h2>Protokoll</h2><pre>' . hsc(implode(„\n“, array_slice($s['log'], -40))) . '</pre>';
edw_page('Import-Pakete einspielen', $body); </code>
