Fix #belege-umzug: Übernahme aus uploads/ führt Verzeichnisse zusammen statt sie zu verschieben
This commit is contained in:
parent
a7e379b6bf
commit
be234eeb2c
5 changed files with 204 additions and 43 deletions
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-01 – Belege blieben nach dem Umzug nach data/ unauffindbar
|
||||||
|
|
||||||
|
- Beim Umzug der Nutzdaten nach `data/` (#150) wurden die Belege mit `mv uploads/2026 data/belege/` verschoben. Existierte im Ziel bereits ein Jahresverzeichnis – etwa weil vorher schon ein Container gelaufen war –, scheiterte `mv` (bzw. legte den Baum als `data/belege/2026/2026` ab). Weil alle Fehler mit `2>/dev/null || true` unterdrückt wurden, meldete die Migration trotzdem Erfolg: In der Oberfläche waren die Belege danach nicht mehr auffindbar, obwohl die Datenbankeinträge unverändert waren.
|
||||||
|
- **`scripts/uploads-uebernahme.sh`** (neu): Übernimmt den Altbestand jetzt Datei für Datei und führt bestehende Verzeichnisse zusammen, statt ganze Verzeichnisse zu verschieben. Eine bereits vorhandene Zieldatei wird nie überschrieben – die Quelldatei bleibt in `uploads/` zur Prüfung liegen und die Kollision wird gemeldet. Verbleiben Reste, endet das Skript mit Exit-Code 1 statt mit einer Erfolgsmeldung.
|
||||||
|
- **`docker-entrypoint.sh`, `scripts/migrate-datenverzeichnis.sh`:** Beide rufen dieselbe Übernahme-Logik auf, damit Container-Start und manueller Umzug nicht auseinanderdriften. Da `scripts/` als Volume gemountet ist, der Entrypoint dagegen aus dem Image stammt, wirkt eine Korrektur am Umzug ohne Rebuild.
|
||||||
|
- **`tests/76_DatenverzeichnisUebernahmeTest.php`** (neu): Führt die Übernahme real gegen ein Zielverzeichnis aus, in dem das Jahresverzeichnis bereits existiert, und sichert Zusammenführung statt Verschachtelung, unangetastete Zieldateien, die Trennung von Logo und Belegen sowie die Exit-Codes ab.
|
||||||
|
|
||||||
## 2026-08-01 – Analyse-Skript für die Nextcloud-Verbindung
|
## 2026-08-01 – Analyse-Skript für die Nextcloud-Verbindung
|
||||||
|
|
||||||
- **`scripts/analyse_nextcloud.sh`** (neu): Grenzt Störungen des Belegimports ein, ohne dass dafür Einzeiler auf dem Server nötig sind. Das Skript liest die hinterlegte Konfiguration (Passwort nur als Länge und Verschlüsselungsstatus, nie im Klartext), zeigt Namensauflösung und IP-Pinning des SSRF-Schutzes, unternimmt einen PROPFIND auf den Import-Ordner, listet dessen Inhalt inklusive der Dateien, die wegen ihres Typs übersprungen würden, und prüft die Erreichbarkeit des Erledigte-Ordners.
|
- **`scripts/analyse_nextcloud.sh`** (neu): Grenzt Störungen des Belegimports ein, ohne dass dafür Einzeiler auf dem Server nötig sind. Das Skript liest die hinterlegte Konfiguration (Passwort nur als Länge und Verschlüsselungsstatus, nie im Klartext), zeigt Namensauflösung und IP-Pinning des SSRF-Schutzes, unternimmt einen PROPFIND auf den Import-Ordner, listet dessen Inhalt inklusive der Dateien, die wegen ihres Typs übersprungen würden, und prüft die Erreichbarkeit des Erledigte-Ordners.
|
||||||
|
|
|
||||||
|
|
@ -14,38 +14,22 @@ mkdir -p "$BELEGE" "$LOGO" 2>/dev/null || true
|
||||||
# Installation endet, deren Dateien nicht mehr gefunden werden.
|
# Installation endet, deren Dateien nicht mehr gefunden werden.
|
||||||
# Idempotent: Nach dem Umzug sind die Quellverzeichnisse leer.
|
# Idempotent: Nach dem Umzug sind die Quellverzeichnisse leer.
|
||||||
|
|
||||||
# Logo aus dem alten Web-Root (Stand vor Fix #116)
|
# Die eigentliche Übernahme liegt in scripts/uploads-uebernahme.sh – dieses
|
||||||
if [ -d "$WEBROOT_UPLOADS/logo" ]; then
|
# Verzeichnis ist als Volume gemountet, der Entrypoint dagegen steckt im Image.
|
||||||
for f in "$WEBROOT_UPLOADS"/logo/*; do
|
# So wirkt eine Korrektur am Umzug auch ohne Rebuild.
|
||||||
[ -e "$f" ] || continue
|
UEBERNAHME=/var/www/html/scripts/uploads-uebernahme.sh
|
||||||
echo "Migration: $(basename "$f") aus dem Web-Root -> data/logo/"
|
|
||||||
mv "$f" "$LOGO/" 2>/dev/null || true
|
# Logo aus dem alten Web-Root (Stand vor Fix #116). Übergeben wird direkt der
|
||||||
done
|
# logo-Ordner – alles darin ist Logo, deshalb steht data/logo an beiden Zielstellen.
|
||||||
|
# Andere Dateien im Web-Root bleiben so unangetastet.
|
||||||
|
if [ -d "$WEBROOT_UPLOADS/logo" ] && [ -n "$(ls -A "$WEBROOT_UPLOADS/logo" 2>/dev/null)" ]; then
|
||||||
|
echo "Migration: Logo aus dem Web-Root -> data/logo/"
|
||||||
|
sh "$UEBERNAHME" "$WEBROOT_UPLOADS/logo" "$LOGO" "$LOGO" || true
|
||||||
rmdir "$WEBROOT_UPLOADS/logo" 2>/dev/null || true
|
rmdir "$WEBROOT_UPLOADS/logo" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Gemeinsame uploads-Ablage (Stand nach #116, vor der Trennung in belege/logo)
|
# Gemeinsame uploads-Ablage (Stand nach #116, vor der Trennung in belege/logo)
|
||||||
if [ -d "$LEGACY" ] && [ -n "$(ls -A "$LEGACY" 2>/dev/null)" ]; then
|
sh "$UEBERNAHME" "$LEGACY" "$BELEGE" "$LOGO" || true
|
||||||
echo "Migration: Altbestand aus uploads/ wird übernommen..."
|
|
||||||
|
|
||||||
# Logo zuerst, damit es nicht als Beleg mitwandert
|
|
||||||
if [ -d "$LEGACY/logo" ]; then
|
|
||||||
for f in "$LEGACY"/logo/*; do
|
|
||||||
[ -e "$f" ] || continue
|
|
||||||
echo " Logo: $(basename "$f") -> data/logo/"
|
|
||||||
mv "$f" "$LOGO/" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
rmdir "$LEGACY/logo" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
for entry in "$LEGACY"/*; do
|
|
||||||
[ -e "$entry" ] || continue
|
|
||||||
echo " Beleg: $(basename "$entry") -> data/belege/"
|
|
||||||
mv "$entry" "$BELEGE/" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Migration: Altbestand übernommen."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fix ownership of volume-mounted data directories at container start
|
# Fix ownership of volume-mounted data directories at container start
|
||||||
chown -R www-data:www-data "$BELEGE" "$LOGO" 2>/dev/null || true
|
chown -R www-data:www-data "$BELEGE" "$LOGO" 2>/dev/null || true
|
||||||
|
|
|
||||||
|
|
@ -46,24 +46,12 @@ fi
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$ROOT/uploads:/alt" \
|
-v "$ROOT/uploads:/alt" \
|
||||||
-v "$ROOT/data:/neu" \
|
-v "$ROOT/data:/neu" \
|
||||||
|
-v "$ROOT/scripts:/skripte:ro" \
|
||||||
alpine:3 sh -eu -c '
|
alpine:3 sh -eu -c '
|
||||||
mkdir -p /neu/belege /neu/logo
|
mkdir -p /neu/belege /neu/logo
|
||||||
|
|
||||||
# Logo zuerst, damit es nicht als Beleg mitwandert
|
# Gemeinsame Übernahme-Logik – dieselbe, die auch beim Container-Start läuft.
|
||||||
if [ -d /alt/logo ]; then
|
sh /skripte/uploads-uebernahme.sh /alt /neu/belege /neu/logo || true
|
||||||
for f in /alt/logo/*; do
|
|
||||||
[ -e "$f" ] || continue
|
|
||||||
echo "Logo: $(basename "$f") -> data/logo/"
|
|
||||||
mv "$f" /neu/logo/
|
|
||||||
done
|
|
||||||
rmdir /alt/logo 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
for entry in /alt/*; do
|
|
||||||
[ -e "$entry" ] || continue
|
|
||||||
echo "Beleg: $(basename "$entry") -> data/belege/"
|
|
||||||
mv "$entry" /neu/belege/
|
|
||||||
done
|
|
||||||
|
|
||||||
chown -R 33:33 /neu/belege /neu/logo
|
chown -R 33:33 /neu/belege /neu/logo
|
||||||
|
|
||||||
|
|
|
||||||
67
scripts/uploads-uebernahme.sh
Executable file
67
scripts/uploads-uebernahme.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Übernimmt den Altbestand aus dem gemeinsamen uploads-Verzeichnis in die
|
||||||
|
# getrennten Datenverzeichnisse data/belege und data/logo.
|
||||||
|
#
|
||||||
|
# scripts/uploads-uebernahme.sh <uploads-verzeichnis> <belege-verzeichnis> <logo-verzeichnis>
|
||||||
|
#
|
||||||
|
# Aufrufer sind docker-entrypoint.sh (bei jedem Container-Start) und
|
||||||
|
# scripts/migrate-datenverzeichnis.sh (manueller Umzug). Die Logik liegt bewusst
|
||||||
|
# hier und nicht doppelt in beiden Skripten.
|
||||||
|
#
|
||||||
|
# Idempotent: Ist der Altbestand übernommen, findet ein zweiter Lauf nichts mehr.
|
||||||
|
# Exit-Code 0 = nichts zu tun oder vollständig übernommen, 1 = Reste verblieben.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
LEGACY="${1:?uploads-Verzeichnis fehlt}"
|
||||||
|
BELEGE="${2:?belege-Verzeichnis fehlt}"
|
||||||
|
LOGO="${3:?logo-Verzeichnis fehlt}"
|
||||||
|
|
||||||
|
# Verschiebt den Inhalt von $1 nach $2 und führt dabei bestehende Verzeichnisse
|
||||||
|
# zusammen. Ein "mv uploads/2026 data/belege/" genügt nicht: Existiert
|
||||||
|
# data/belege/2026 schon, scheitert mv – oder legt den Baum als
|
||||||
|
# data/belege/2026/2026 ab. Beides blieb früher unbemerkt, weil die Fehler
|
||||||
|
# unterdrückt wurden, und die Belege waren anschließend nicht mehr auffindbar.
|
||||||
|
# Deshalb Datei für Datei, ohne bestehende Ziele zu überschreiben.
|
||||||
|
merge_tree() {
|
||||||
|
src="$1"
|
||||||
|
dst="$2"
|
||||||
|
( cd "$src" && find . -mindepth 1 -type d -exec mkdir -p "$dst/{}" \; )
|
||||||
|
( cd "$src" && find . -mindepth 1 -type f ) | while IFS= read -r f; do
|
||||||
|
rel="${f#./}"
|
||||||
|
if [ -e "$dst/$rel" ]; then
|
||||||
|
echo " übersprungen (Ziel existiert bereits): $rel" >&2
|
||||||
|
elif mv "$src/$rel" "$dst/$rel"; then
|
||||||
|
echo " $rel"
|
||||||
|
else
|
||||||
|
echo " FEHLER beim Verschieben: $rel" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
find "$src" -mindepth 1 -depth -type d -empty -exec rmdir {} + 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -d "$LEGACY" ] || [ -z "$(ls -A "$LEGACY" 2>/dev/null)" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BELEGE" "$LOGO"
|
||||||
|
|
||||||
|
echo "Migration: Altbestand aus uploads/ wird übernommen..."
|
||||||
|
|
||||||
|
# Logo zuerst, damit es nicht als Beleg mitwandert
|
||||||
|
if [ -d "$LEGACY/logo" ]; then
|
||||||
|
echo " Logo -> data/logo/"
|
||||||
|
merge_tree "$LEGACY/logo" "$LOGO"
|
||||||
|
rmdir "$LEGACY/logo" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Belege -> data/belege/"
|
||||||
|
merge_tree "$LEGACY" "$BELEGE"
|
||||||
|
|
||||||
|
verblieben=$(find "$LEGACY" -mindepth 1 -type f 2>/dev/null | wc -l)
|
||||||
|
if [ "$verblieben" -gt 0 ]; then
|
||||||
|
echo "Migration: ACHTUNG – $verblieben Datei(en) verbleiben in uploads/ und müssen von Hand geprüft werden." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Migration: Altbestand vollständig übernommen."
|
||||||
115
tests/76_DatenverzeichnisUebernahmeTest.php
Normal file
115
tests/76_DatenverzeichnisUebernahmeTest.php
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 76: Übernahme des Altbestands aus uploads/ nach data/belege.
|
||||||
|
*
|
||||||
|
* Der Umzug lief früher über "mv uploads/2026 data/belege/". Existierte im Ziel
|
||||||
|
* bereits ein Jahresverzeichnis, scheiterte das Verschieben (bzw. legte den Baum
|
||||||
|
* als data/belege/2026/2026 ab) – und weil die Fehler mit "2>/dev/null || true"
|
||||||
|
* unterdrückt wurden, meldete die Migration trotzdem Erfolg. Ergebnis: eine
|
||||||
|
* Installation, in der die Belege nicht mehr auffindbar waren.
|
||||||
|
*
|
||||||
|
* Der Test führt scripts/uploads-uebernahme.sh real aus und prüft, dass es
|
||||||
|
* zusammenführt statt zu verschachteln und keine vorhandene Zieldatei überschreibt.
|
||||||
|
*/
|
||||||
|
class DatenverzeichnisUebernahmeTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 76: Übernahme Altbestand uploads/ -> data/belege';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$skript = dirname(__DIR__) . '/scripts/uploads-uebernahme.sh';
|
||||||
|
|
||||||
|
$this->assert(file_exists($skript), 'scripts/uploads-uebernahme.sh existiert');
|
||||||
|
exec('sh -n ' . escapeshellarg($skript) . ' 2>&1', $syntax, $syntaxExit);
|
||||||
|
$this->assertEquals(0, $syntaxExit, 'sh-Syntax ist gültig (' . implode(' ', $syntax) . ')');
|
||||||
|
|
||||||
|
$inhalt = (string)file_get_contents($skript);
|
||||||
|
$this->assert(
|
||||||
|
!preg_match('/\bmv\b[^\n]*2>\/dev\/null\s*\|\|\s*true/', $inhalt),
|
||||||
|
'Fehler beim Verschieben werden nicht unterdrückt'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Der manuelle Umzug muss dieselbe Logik verwenden wie der Container-Start –
|
||||||
|
// sonst driftet beides auseinander. docker-entrypoint.sh wird hier bewusst
|
||||||
|
// nicht geprüft: Die Datei ist nicht als Volume gemountet, im Container läge
|
||||||
|
// also immer der Stand des Images vor und der Test schlüge nur an, wenn ein
|
||||||
|
// Rebuild aussteht.
|
||||||
|
$migration = dirname(__DIR__) . '/scripts/migrate-datenverzeichnis.sh';
|
||||||
|
$this->assert(
|
||||||
|
str_contains((string)file_get_contents($migration), 'uploads-uebernahme.sh'),
|
||||||
|
'migrate-datenverzeichnis.sh verwendet scripts/uploads-uebernahme.sh'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Funktionaler Lauf: Ziel enthält bereits ein Jahresverzeichnis ──────
|
||||||
|
$basis = sys_get_temp_dir() . '/test76_' . bin2hex(random_bytes(6));
|
||||||
|
$alt = $basis . '/uploads';
|
||||||
|
$neu = $basis . '/belege';
|
||||||
|
$logo = $basis . '/logo';
|
||||||
|
|
||||||
|
mkdir($alt . '/2026/BLG-2026-0001', 0777, true);
|
||||||
|
mkdir($alt . '/2026/BLG-2026-0099', 0777, true);
|
||||||
|
mkdir($alt . '/logo', 0777, true);
|
||||||
|
mkdir($neu . '/2026/BLG-2026-0001', 0777, true);
|
||||||
|
mkdir($logo, 0777, true);
|
||||||
|
file_put_contents($alt . '/2026/BLG-2026-0001/original.pdf', 'ALT');
|
||||||
|
file_put_contents($alt . '/2026/BLG-2026-0099/original.pdf', 'NEU-IM-ALTBESTAND');
|
||||||
|
file_put_contents($alt . '/logo/logo.png', 'LOGO');
|
||||||
|
file_put_contents($neu . '/2026/BLG-2026-0001/original.pdf', 'BESTEHEND');
|
||||||
|
|
||||||
|
$befehl = 'sh ' . escapeshellarg($skript) . ' '
|
||||||
|
. escapeshellarg($alt) . ' ' . escapeshellarg($neu) . ' ' . escapeshellarg($logo) . ' 2>&1';
|
||||||
|
exec($befehl, $ausgabe, $exitCode);
|
||||||
|
$ausgabeText = implode("\n", $ausgabe);
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
is_file($neu . '/2026/BLG-2026-0099/original.pdf'),
|
||||||
|
'Beleg aus dem Altbestand landet im bestehenden Jahresverzeichnis'
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'NEU-IM-ALTBESTAND',
|
||||||
|
(string)file_get_contents($neu . '/2026/BLG-2026-0099/original.pdf'),
|
||||||
|
'Übernommener Beleg ist inhaltlich unverändert'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
!is_dir($neu . '/2026/2026'),
|
||||||
|
'Der Baum wird nicht als data/belege/2026/2026 verschachtelt'
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'LOGO',
|
||||||
|
(string)file_get_contents($logo . '/logo.png'),
|
||||||
|
'Das Logo wandert nach data/logo und nicht in die Belege'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
!is_file($neu . '/logo/logo.png'),
|
||||||
|
'Das Logo liegt nicht zusätzlich als Beleg vor'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kollision: bestehende Zieldatei bleibt, Quelldatei geht nicht verloren.
|
||||||
|
$this->assertEquals(
|
||||||
|
'BESTEHEND',
|
||||||
|
(string)file_get_contents($neu . '/2026/BLG-2026-0001/original.pdf'),
|
||||||
|
'Vorhandene Zieldatei wird nicht überschrieben'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
is_file($alt . '/2026/BLG-2026-0001/original.pdf'),
|
||||||
|
'Kollidierende Quelldatei bleibt zur Prüfung liegen statt verloren zu gehen'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
str_contains($ausgabeText, 'übersprungen'),
|
||||||
|
'Die Kollision wird gemeldet und nicht stillschweigend übergangen'
|
||||||
|
);
|
||||||
|
$this->assertEquals(1, $exitCode, 'Verbliebene Reste melden einen Fehler-Exitcode');
|
||||||
|
|
||||||
|
// ── Zweiter Lauf ohne Altbestand: nichts zu tun, sauberer Exitcode ────
|
||||||
|
$leer = $basis . '/leer';
|
||||||
|
mkdir($leer, 0777, true);
|
||||||
|
exec('sh ' . escapeshellarg($skript) . ' ' . escapeshellarg($leer) . ' '
|
||||||
|
. escapeshellarg($neu) . ' ' . escapeshellarg($logo) . ' 2>&1', $zweite, $zweiterExit);
|
||||||
|
$this->assertEquals(0, $zweiterExit, 'Lauf ohne Altbestand endet mit Exitcode 0');
|
||||||
|
|
||||||
|
exec('rm -rf ' . escapeshellarg($basis));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue