feat(nextcloud-files): Hoch- und Herunterladen als Datenstrom, große Dateien in Stücken, ZIP
- Hochladen ohne Pufferung: ganze Datei oder Chunked Upload v2 in 8-MiB-Stücken, Zusammenbau im Hintergrund mit abfragbarem Zustand - Überschreibschutz (If-None-Match / Entity-Tag-Prüfung), Download mit Range, Content-Disposition und Schutzkopfzeilen von Tessera selbst - Ordner- und Auswahl-ZIP; jeder Nextcloud-Fehler wird vor dem ersten Byte abgebildet, nie 401/403 - Browser-Hochlader mit Fortschritt, Wiederholung, Abbruch und Aufräumen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
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;
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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;
|
||||
}
|
||||
|
||||
export class ZipQueryDto {
|
||||
/** 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]))
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(500)
|
||||
@IsString({ each: true })
|
||||
@MaxLength(255, { each: true })
|
||||
name!: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user