Fix #belege-umzug: Übernahme aus uploads/ führt Verzeichnisse zusammen statt sie zu verschieben

This commit is contained in:
fds2610-bot 2026-08-01 15:15:41 +02:00
parent a7e379b6bf
commit be234eeb2c
5 changed files with 204 additions and 43 deletions

View file

@ -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
- **`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.

View file

@ -14,38 +14,22 @@ mkdir -p "$BELEGE" "$LOGO" 2>/dev/null || true
# Installation endet, deren Dateien nicht mehr gefunden werden.
# Idempotent: Nach dem Umzug sind die Quellverzeichnisse leer.
# Logo aus dem alten Web-Root (Stand vor Fix #116)
if [ -d "$WEBROOT_UPLOADS/logo" ]; then
for f in "$WEBROOT_UPLOADS"/logo/*; do
[ -e "$f" ] || continue
echo "Migration: $(basename "$f") aus dem Web-Root -> data/logo/"
mv "$f" "$LOGO/" 2>/dev/null || true
done
# Die eigentliche Übernahme liegt in scripts/uploads-uebernahme.sh – dieses
# Verzeichnis ist als Volume gemountet, der Entrypoint dagegen steckt im Image.
# So wirkt eine Korrektur am Umzug auch ohne Rebuild.
UEBERNAHME=/var/www/html/scripts/uploads-uebernahme.sh
# Logo aus dem alten Web-Root (Stand vor Fix #116). Übergeben wird direkt der
# 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
fi
# Gemeinsame uploads-Ablage (Stand nach #116, vor der Trennung in belege/logo)
if [ -d "$LEGACY" ] && [ -n "$(ls -A "$LEGACY" 2>/dev/null)" ]; then
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
sh "$UEBERNAHME" "$LEGACY" "$BELEGE" "$LOGO" || true
# Fix ownership of volume-mounted data directories at container start
chown -R www-data:www-data "$BELEGE" "$LOGO" 2>/dev/null || true

View file

@ -46,24 +46,12 @@ fi
docker run --rm \
-v "$ROOT/uploads:/alt" \
-v "$ROOT/data:/neu" \
-v "$ROOT/scripts:/skripte:ro" \
alpine:3 sh -eu -c '
mkdir -p /neu/belege /neu/logo
# Logo zuerst, damit es nicht als Beleg mitwandert
if [ -d /alt/logo ]; then
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
# Gemeinsame Übernahme-Logik – dieselbe, die auch beim Container-Start läuft.
sh /skripte/uploads-uebernahme.sh /alt /neu/belege /neu/logo || true
chown -R 33:33 /neu/belege /neu/logo

67
scripts/uploads-uebernahme.sh Executable file
View 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."

View 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));
}
}