Files
tessera-ctl/apps/api/src/nextcloud-files/dto/nextcloud-files-transfer.dto.ts
T
schalli 8679668cac 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>
2026-10-08 22:50:24 +02:00

163 lines
3.9 KiB
TypeScript

import { Transform, Type } from 'class-transformer';
import {
ArrayMaxSize,
ArrayMinSize,
IsArray,
IsIn,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Matches,
Max,
MaxLength,
Min,
} from 'class-validator';
/**
* Eingaben der Uebertragungsrouten (quick-261008-mzu, D-J/D-K). Pfade stehen nur
* in Query oder Body, nie im URL-Pfad der Tessera-Route. Die Segmentpruefung macht
* der Dienst (`parseUserPath`, `validateSegment`); hier stehen nur grobe Grenzen.
*
* Absichtlich KEIN `@Max` auf `size`: eine zu grosse Datei soll mit 413
* `fileTooLarge` und lesbarem Text abgelehnt werden, nicht mit einem 400 der
* allgemeinen Pruefung.
*/
/** Hoechster Unix-Zeitstempel (Sekunden), den eine Aenderungszeit haben darf (Jahr 2100). */
export const MAX_MTIME_SECONDS = 4102444800;
/**
* Form eines Entity-Tags (IN-01): optional `W/`, optional in Anfuehrungszeichen, sonst nur
* sichtbare ASCII-Zeichen. Der Wert geht als `If-Match`-Kopfzeile an Nextcloud; CR, LF oder
* andere Steuerzeichen liessen den Aufruf sonst scheitern und als "nicht erreichbar" enden.
*/
export const ETAG_PATTERN = /^(W\/)?"?[\x21\x23-\x7e]{1,128}"?$/;
export class StartUploadDto {
@IsString()
@IsNotEmpty()
@MaxLength(4096)
path!: string;
@IsInt()
@Min(0)
size!: number;
/** Entity-Tag der Version, die ersetzt werden soll (nach Rueckfrage beim Benutzer). */
@IsOptional()
@IsString()
@MaxLength(200)
@Matches(ETAG_PATTERN)
replaceEtag?: string;
}
export class CompleteUploadDto {
@IsString()
@IsNotEmpty()
@MaxLength(4096)
path!: string;
@IsInt()
@Min(0)
size!: number;
@IsOptional()
@IsInt()
@Min(0)
@Max(MAX_MTIME_SECONDS)
mtime?: number;
@IsOptional()
@IsString()
@MaxLength(200)
@Matches(ETAG_PATTERN)
replaceEtag?: string;
}
/** Query der Raw-Body-Routen `PUT uploads/file` und `PUT uploads/:uploadId/chunks/:n`. */
export class UploadQueryDto {
@IsString()
@IsNotEmpty()
@MaxLength(4096)
path!: string;
/** Gesamtgroesse der Datei in Byte. */
@Type(() => Number)
@IsInt()
@Min(0)
size!: number;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
@Max(MAX_MTIME_SECONDS)
mtime?: number;
@IsOptional()
@IsString()
@MaxLength(200)
@Matches(ETAG_PATTERN)
replaceEtag?: string;
}
export class DownloadQueryDto {
@IsString()
@IsNotEmpty()
@MaxLength(4096)
path!: string;
/** `1`: den Ordner als ZIP laden statt einer Datei. */
@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;
}
/** 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;
@Transform(({ value }) => {
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch {
return value;
}
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(ZIP_MAX_NAMES)
@IsString({ each: true })
@MaxLength(255, { each: true })
names!: string[];
/** `true` (JSON) oder `1` (Formular): nur pruefen, kein ZIP (IN-06). */
@IsOptional()
@IsIn([true, '1'])
check?: boolean | string;
}