Benutzer-Werkzeuge

Webseiten-Werkzeuge


ki:galnet:werkzeuge:edwiki_import

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

  1. 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.
  2. Beliebig viele Pakete nacheinander; gitbacked bleibt dazwischen aus (gelber Hinweis oben).
  3. Zum Schluss einmal Abschließen: Suchindex für alle eingespielten Seiten, gitbacked an, Link zu edwiki-git-sync.php.
  4. Läuft in Häppchen von 20 s (Seite lädt sich selbst neu). Bei Abbruch (Browser zu, 504): Seite neu öffnen → „Fortsetzen“.
  5. 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&sectok=' . 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&sectok=' . 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>

1)
$action || $go) && !checkSecurityToken(
ki/galnet/werkzeuge/edwiki_import.txt · Zuletzt geändert: (Externe Bearbeitung)