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
@@ -112,21 +112,51 @@ export class DownloadQueryDto {
@IsOptional()
@IsIn(['1'])
zip?: string;
/**
* `1`: nur pruefen (Verbindung, Eintrag vorhanden), keinen Datenstrom liefern (IN-06). Die
* Weboberflaeche fragt so vor dem eigentlichen Herunterladen, weil sie Fehler eines
* Downloads im Browser sonst nicht sieht.
*/
@IsOptional()
@IsIn(['1'])
check?: string;
}
export class ZipQueryDto {
/** Hoechstzahl gewaehlter Eintraege fuer ein Auswahl-ZIP (WR-08). */
export const ZIP_MAX_NAMES = 1000;
/**
* Auswahl-ZIP (WR-08): die Namen kommen im KOERPER eines POST, nicht in der Adresse — mehrere
* hundert Namen sprengten sonst die Kopfzeilen-Grenze von Node (431) oder den Proxy (414).
* Als JSON (`names` ist eine Liste) oder aus einem unsichtbaren Formular der Weboberflaeche
* (`names` ist dieselbe Liste als JSON-Text; so laedt der Browser das ZIP als Strom direkt
* auf die Platte, statt es im Arbeitsspeicher zu sammeln).
*/
export class ZipBodyDto {
/** Ordner, aus dem die gewaehlten Eintraege kommen (`/` = Wurzel). */
@IsOptional()
@IsString()
@MaxLength(4096)
dir?: string;
/** Namen der gewaehlten Eintraege; ein einzelnes `name=...` wird zur Liste. */
@Transform(({ value }) => (Array.isArray(value) ? value : value === undefined ? [] : [value]))
@Transform(({ value }) => {
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch {
return value;
}
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(500)
@ArrayMaxSize(ZIP_MAX_NAMES)
@IsString({ each: true })
@MaxLength(255, { each: true })
name!: string[];
names!: string[];
/** `true` (JSON) oder `1` (Formular): nur pruefen, kein ZIP (IN-06). */
@IsOptional()
@IsIn([true, '1'])
check?: boolean | string;
}