fix(nextcloud-files): WR-08/IN-06 Auswahl-ZIP per POST, Downloads mit Vorabpruefung

- WR-08: POST download/zip mit den Namen im Koerper (JSON oder unsichtbares Formular mit
  names als JSON-Text) statt im Query-String; hoechstens 1000 Namen; die Weboberflaeche
  laedt per Formular in einen unsichtbaren Rahmen (Strom direkt auf die Platte, Cookie und
  Content-Disposition bleiben); alles ausgewaehlt -> ganzer Ordner als ZIP
- Nextcloud nimmt die Auswahl nur in ihrer Adresse an (gemessen: Apache lehnt ueber 8190
  Zeichen mit 414 ab); Tessera prueft die Laenge und antwortet vorher mit 413
  selectionTooLarge, die Weboberflaeche meldet es schon vor dem Absenden
- IN-06: Vorabpruefung (check=1 bzw. check: true, ein PROPFIND Depth 0) vor jedem Download;
  Fehler stehen in der Statuszeile, connectionExpired fuehrt zum Anmeldebildschirm; auch der
  Klick auf einen Dateinamen geht ueber die Pruefung
- e2e-transfer auf POST umgestellt (JSON, Formular ueber /api-proxy, 300 Namen, Pruefung),
  Anwenderhandbuch

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 22:50:24 +02:00
parent 486819f80b
commit 8679668cac
16 changed files with 499 additions and 56 deletions
+43 -5
View File
@@ -269,11 +269,49 @@ export function downloadUrl(path: string, opts: { zip?: boolean } = {}): string
return `${API_URL}${BASE}/download?path=${encodeURIComponent(path)}${zip}`;
}
/** Hoechstzahl gewaehlter Eintraege fuer ein Auswahl-ZIP (wie die API, WR-08). */
export const ZIP_MAX_NAMES = 1000;
/**
* Adresse zum Herunterladen mehrerer Eintraege eines Ordners als ein ZIP. Jeder Name
* steht als eigener `name`-Parameter (wiederholter Schluessel), einzeln codiert.
* Budget fuer Ordner und Namensliste, codiert wie in der Adresse an Nextcloud. Nextcloud nimmt
* die Auswahl nur in der Adresse an, und ein Apache davor lehnt Anfragezeilen ueber 8190
* Zeichen ab; die API prueft verbindlich (413 `selectionTooLarge`), hier wird vorher und
* etwas vorsichtiger gerechnet, damit der Benutzer sofort eine klare Meldung bekommt.
*/
export function zipUrl(dir: string, names: readonly string[]): string {
const list = names.map((n) => `name=${encodeURIComponent(n)}`).join('&');
return `${API_URL}${BASE}/download/zip?dir=${encodeURIComponent(dir)}&${list}`;
export const ZIP_MAX_ENCODED_CHARS = 7000;
/**
* Ziel des Auswahl-ZIPs (WR-08): POST mit den Namen im KOERPER, nicht in der Adresse —
* mehrere hundert Namen sprengten sonst die Grenzen von Node (431) oder des Proxys (414).
*/
export function zipPostUrl(): string {
return `${API_URL}${BASE}/download/zip`;
}
/** Formularfelder des Auswahl-ZIPs: Ordner und die Namen als JSON-Liste. */
export function zipFormFields(dir: string, names: readonly string[]): Record<string, string> {
return { dir, names: JSON.stringify(names) };
}
/** Passt die Auswahl in ein Auswahl-ZIP (Anzahl und Laenge der Adresse an Nextcloud)? */
export function zipSelectionFits(dir: string, names: readonly string[]): boolean {
if (names.length === 0 || names.length > ZIP_MAX_NAMES) return false;
const encoded = encodeURIComponent(dir).length + encodeURIComponent(JSON.stringify(names)).length;
return encoded <= ZIP_MAX_ENCODED_CHARS;
}
/**
* Vorabpruefung eines Downloads (IN-06): Verbindung und Eintrag. Ein Download ueber einen Link
* zeigt Fehler im Browser nicht an; so kann die Ansicht sie melden.
*/
export function checkDownload(path: string, opts: { zip?: boolean } = {}): Promise<{ ok: true }> {
const zip = opts.zip ? '&zip=1' : '';
return request<{ ok: true }>(`/download?path=${encodeURIComponent(path)}${zip}&check=1`);
}
/** Vorabpruefung eines Auswahl-ZIPs (IN-06): Verbindung, Ordner und die Namen selbst. */
export function checkZip(dir: string, names: readonly string[]): Promise<{ ok: true }> {
return request<{ ok: true }>('/download/zip', {
method: 'POST',
json: { dir, names, check: true },
});
}