8679668cac
- 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>
163 lines
3.9 KiB
TypeScript
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;
|
|
}
|