feat(nextcloud-files): Dateien auflisten, Vorschau, Ordner anlegen, umbenennen, verschieben und löschen

- PROPFIND-Auswertung (mehrere propstat, Kleinbuchstaben-Hex, Speicher -3 = unbegrenzt, nur Zeichenketten)
- WebDAV-Schicht mit Zugangsschluessel und Aufrufsperre, Destination immer aus der Basis, Overwrite: F
- mapNcFailure als einzige Fehlerabbildung (nie 401/403 an den Browser), sendUpstreamStream prueft vor dem ersten Byte
- Dateidienst und Routen files, folders, move, preview im eigenen Konto; zweiter Benutzer bekommt notConnected
- Web-API-Funktionen listFolder, createFolder, moveEntry, deleteEntry, previewUrl
- e2e-files.sh gegen die Test-Nextcloud

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 18:14:54 +02:00
parent d00b6ff79f
commit 8bee65c306
18 changed files with 2214 additions and 5 deletions
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# Aufgabe 3: Datei-Schnittstelle gegen die Test-Nextcloud (Liste mit Speicher, Vorschau, Ordner,
# Verschieben/Umbenennen, Papierkorb, zweiter Benutzer). Setzt einen laufenden Stack und
# nc-test-setup.sh voraus. Nur Testwerte; liest keine .env-Dateien.
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/e2e-lib.sh"
trap 'rm -rf "$E2E_TMP"' EXIT
FILES="$API/modules/nextcloud-files"
NC_DAV="http://localhost:18080/remote.php/dav"
ANNA="anna:User1-Pass-12345"
# 1x1-PNG als Base64 (Vorschau)
PNG_B64='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
UMLAUT_NAME='Ärger & Ölpreis 100%.txt'
UMLAUT_ENC='%C3%84rger%20%26%20%C3%96lpreis%20100%25.txt'
ADMIN="$E2E_TMP/admin.jar"
e2e_wait_health
e2e_login "$ADMIN"
e2e_activate "$ADMIN"
e2e_set_address "$ADMIN"
code=$(e2e_connect_anna "$ADMIN")
e2e_expect 200 "$code" "anna verbinden"
# GET <jar> <endpoint> <pfad> — Pfad als Query-Parameter, sauber codiert; Status auf stdout.
qget() { curl -s -o "$E2E_TMP/body.out" -w '%{http_code}' -b "$1" -G --data-urlencode "path=$3" "$FILES/$2"; }
qdel() { curl -s -o "$E2E_TMP/body.out" -w '%{http_code}' -b "$1" -X DELETE -G --data-urlencode "path=$3" "$FILES/$2"; }
post() { e2e_status "$1" POST "$FILES/$2" "$3"; }
# JSON-Body: die Umlautdatei im Testordner ins Unterverzeichnis Ziel verschieben
move_umlaut_json() {
python3 -I -c 'import json,sys
print(json.dumps({"from": sys.argv[1] + "/" + sys.argv[2], "to": sys.argv[1] + "/Ziel/" + sys.argv[2]}))' "$F" "$UMLAUT_NAME"
}
F="/Tessera-E2E-$(date +%s)"
FNAME="${F#/}"
# --- Vorbereitung direkt in der Nextcloud (mit annas Passwort) --------------------------------
nc_put() { curl -s -o /dev/null -w '%{http_code}' -u "$ANNA" -X PUT --data-binary "$2" "$NC_DAV/files/anna$1"; }
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$ANNA" -X MKCOL "$NC_DAV/files/anna$F"); e2e_expect 201 "$code" "Fixture-Ordner"
code=$(nc_put "$F/$UMLAUT_ENC" "hallo"); e2e_expect 201 "$code" "Fixture Umlautdatei"
echo "$PNG_B64" | base64 -d > "$E2E_TMP/pixel.png"
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$ANNA" -X PUT -H 'Content-Type: image/png' \
--data-binary "@$E2E_TMP/pixel.png" "$NC_DAV/files/anna$F/pixel.png"); e2e_expect 201 "$code" "Fixture PNG"
# --- Liste der Wurzel mit Speicherangabe ---------------------------------------------------------
code=$(qget "$ADMIN" files "/"); e2e_expect 200 "$code" "Wurzel auflisten"
e2e_contains "$E2E_TMP/body.out" '"entries"' "Wurzel: entries"
e2e_contains "$E2E_TMP/body.out" '"quota"' "Wurzel: quota"
e2e_contains "$E2E_TMP/body.out" "\"name\":\"$FNAME\"" "Wurzel enthaelt den Testordner"
# --- Liste des Testordners: Umlautname, PNG mit Vorschau -------------------------------------------
code=$(qget "$ADMIN" files "$F"); e2e_expect 200 "$code" "Testordner auflisten"
e2e_contains "$E2E_TMP/body.out" "\"name\":\"$UMLAUT_NAME\"" "Umlautname exakt"
e2e_contains "$E2E_TMP/body.out" '"name":"pixel.png"' "PNG vorhanden"
PNG_ID=$(python3 -I -c 'import json,sys
for e in json.load(open(sys.argv[1]))["entries"]:
if e["name"]=="pixel.png":
assert e["hasPreview"] is True, "hasPreview"
print(e["fileId"])' "$E2E_TMP/body.out")
[ -n "$PNG_ID" ] || e2e_fail "PNG-Kennung fehlt"
# --- Vorschau ---------------------------------------------------------------------------------------
code=$(curl -s -D "$E2E_TMP/prev.hdr" -o "$E2E_TMP/prev.png" -w '%{http_code}' -b "$ADMIN" "$FILES/preview?fileId=$PNG_ID&v=%22x%22")
e2e_expect 200 "$code" "Vorschau"
grep -qi '^content-type: image/png' "$E2E_TMP/prev.hdr" || e2e_fail "Vorschau: kein image/png"
grep -qi '^cache-control: private, max-age=86400' "$E2E_TMP/prev.hdr" || e2e_fail "Vorschau: cache-control"
grep -qi '^set-cookie' "$E2E_TMP/prev.hdr" && e2e_fail "Vorschau: set-cookie aus Nextcloud weitergereicht" || true
code=$(curl -s -o "$E2E_TMP/body.out" -w '%{http_code}' -b "$ADMIN" "$FILES/preview?fileId=abc")
e2e_expect 400 "$code" "Vorschau mit ungueltiger Kennung"
code=$(curl -s -o "$E2E_TMP/body.out" -w '%{http_code}' -b "$ADMIN" "$FILES/preview?fileId=99999999")
e2e_expect 404 "$code" "Vorschau ohne Datei"
# --- Neuer Ordner -----------------------------------------------------------------------------------
code=$(post "$ADMIN" folders "{\"path\":\"$F/Ziel\"}")
case "$code" in 200|201) ;; *) e2e_fail "Ordner anlegen -> $code" ;; esac
code=$(post "$ADMIN" folders "{\"path\":\"$F/Ziel\"}")
e2e_expect 409 "$code" "Ordner doppelt anlegen"
e2e_contains "$E2E_TMP/body.out" '"code":"nameTaken"' "Ordner doppelt: nameTaken"
# --- Verschieben und Konflikt ------------------------------------------------------------------------
code=$(post "$ADMIN" move "$(move_umlaut_json)")
e2e_expect 200 "$code" "Umlautdatei verschieben"
code=$(nc_put "$F/$UMLAUT_ENC" "zweite"); e2e_expect 201 "$code" "zweite Umlautdatei"
code=$(post "$ADMIN" move "$(move_umlaut_json)")
e2e_expect 409 "$code" "Verschieben auf vorhandenen Namen"
e2e_contains "$E2E_TMP/body.out" '"code":"nameTaken"' "Verschieben: nameTaken"
# die verschobene Datei im Ziel ist unveraendert (nicht ueberschrieben)
body=$(curl -s -u "$ANNA" "$NC_DAV/files/anna$F/Ziel/$UMLAUT_ENC")
[ "$body" = "hallo" ] || e2e_fail "Ziel wurde ueberschrieben: '$body'"
# --- Umbenennen ----------------------------------------------------------------------------------------
code=$(post "$ADMIN" move "{\"from\":\"$F/Ziel\",\"to\":\"$F/Ziel2\"}")
e2e_expect 200 "$code" "Ordner umbenennen"
code=$(qget "$ADMIN" files "$F/Ziel2"); e2e_expect 200 "$code" "umbenannter Ordner"
e2e_contains "$E2E_TMP/body.out" "\"name\":\"$UMLAUT_NAME\"" "Datei im umbenannten Ordner"
code=$(post "$ADMIN" move "{\"from\":\"$F\",\"to\":\"$F/Ziel2/innen\"}")
e2e_expect 400 "$code" "Ordner in sich selbst"
e2e_contains "$E2E_TMP/body.out" '"code":"moveIntoItself"' "moveIntoItself"
# --- Ungueltiger Pfad ------------------------------------------------------------------------------------
code=$(qget "$ADMIN" files "/../x"); e2e_expect 400 "$code" "Pfad mit .."
e2e_contains "$E2E_TMP/body.out" '"code":"invalidPath"' "invalidPath"
code=$(qget "$ADMIN" files "/gibt-es-nicht-$(date +%s)"); e2e_expect 404 "$code" "fehlender Ordner"
e2e_contains "$E2E_TMP/body.out" '"code":"notFound"' "notFound"
# --- Zweiter Tessera-Benutzer: kein Zugriff auf annas Konto -------------------------------------------
J2="$E2E_TMP/zweit.jar"
e2e_second_user "nc-e2e-zweit" "$J2"
e2e_grant_use "$ADMIN" "$J2"
code=$(e2e_status "$J2" GET "$FILES/status"); e2e_expect 200 "$code" "Status zweiter Benutzer"
e2e_contains "$E2E_TMP/body.out" '"account":null' "zweiter Benutzer: kein Konto"
code=$(qget "$J2" files "/"); e2e_expect 409 "$code" "Dateien zweiter Benutzer"
e2e_contains "$E2E_TMP/body.out" '"code":"notConnected"' "zweiter Benutzer: notConnected"
code=$(qget "$J2" files "$F"); e2e_expect 409 "$code" "fremder Ordner, zweiter Benutzer"
code=$(qdel "$J2" files "$F"); e2e_expect 409 "$code" "Loeschen, zweiter Benutzer"
code=$(qget "$ADMIN" files "$F"); e2e_expect 200 "$code" "Admin sieht weiter seine Dateien"
# --- Loeschen in den Papierkorb ---------------------------------------------------------------------------
code=$(qdel "$ADMIN" files "$F"); e2e_expect 200 "$code" "Testordner loeschen"
code=$(qget "$ADMIN" files "$F"); e2e_expect 404 "$code" "Testordner ist weg"
curl -s -u "$ANNA" -X PROPFIND -H 'Depth: 1' "$NC_DAV/trashbin/anna/trash" > "$E2E_TMP/trash.xml"
grep -q "$FNAME" "$E2E_TMP/trash.xml" || e2e_fail "Papierkorb enthaelt $FNAME nicht"
# --- Aufraeumen ---------------------------------------------------------------------------------------------
code=$(e2e_status "$ADMIN" DELETE "$FILES/connect"); e2e_expect 200 "$code" "Abmelden"
echo "e2e files ok"
@@ -34,7 +34,11 @@ e2e_status() {
}
# e2e_expect <erwartet> <ist> <beschreibung>
e2e_expect() { [ "$1" = "$2" ] || e2e_fail "$3: erwartet $1, war $2"; }
e2e_expect() {
[ "$1" = "$2" ] && return 0
[ -s "$E2E_TMP/body.out" ] && { echo "--- letzte Antwort:" >&2; head -c 600 "$E2E_TMP/body.out" >&2; echo >&2; }
e2e_fail "$3: erwartet $1, war $2"
}
# e2e_contains <datei> <text> <beschreibung>
e2e_contains() { grep -q -- "$2" "$1" || { echo "--- Antwort:" >&2; cat "$1" >&2; echo >&2; e2e_fail "$3: '$2' fehlt"; }; }