feat(nextcloud-files): Dateien auflisten, Vorschau, Ordner anlegen, umbenennen, verschieben und löschen

- PROPFIND-Auswertung (mehrere propstat, Kleinbuchstaben-Hex, Speicher -3 = unbegrenzt, nur Zeichenketten)
- WebDAV-Schicht mit Zugangsschluessel und Aufrufsperre, Destination immer aus der Basis, Overwrite: F
- mapNcFailure als einzige Fehlerabbildung (nie 401/403 an den Browser), sendUpstreamStream prueft vor dem ersten Byte
- Dateidienst und Routen files, folders, move, preview im eigenen Konto; zweiter Benutzer bekommt notConnected
- Web-API-Funktionen listFolder, createFolder, moveEntry, deleteEntry, previewUrl
- e2e-files.sh gegen die Test-Nextcloud

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 18:14:54 +02:00
parent d00b6ff79f
commit 8bee65c306
18 changed files with 2214 additions and 5 deletions
+12 -1
View File
@@ -169,6 +169,7 @@ export function buildNcUrl(
prefix: NcPrefix,
segments: readonly string[] = [],
query?: Record<string, string>,
trailingSlash = false,
): string {
if (!(ALLOWED_PREFIXES as readonly string[]).includes(prefix)) {
throw new Error(`Nextcloud-Pfadanfang nicht erlaubt: ${prefix}`);
@@ -179,6 +180,8 @@ export function buildNcUrl(
throw new Error(`Nextcloud-Pfadanfang ${prefix} nimmt keine Segmente`);
}
let url = `${base}${prefix}${encodeSegments(segments)}`;
// Ordner-Adressen enden fuer WebDAV mit einem Schraegstrich (PROPFIND auf Ordner).
if (trailingSlash && !url.endsWith('/')) url += '/';
if (query) {
const pairs = Object.entries(query).map(
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`,
@@ -200,6 +203,8 @@ export interface NcRequestOptions {
prefix: NcPrefix;
segments?: readonly string[];
query?: Record<string, string>;
/** Haengt der Adresse einen `/` an (Ordner bei WebDAV). */
trailingSlash?: boolean;
method: string;
headers?: Record<string, string>;
body?: Readable | string | Buffer | null;
@@ -297,7 +302,13 @@ export async function ncRequest(
gate: NextcloudCallGate,
opts: NcRequestOptions,
): Promise<NcResult> {
const url = buildNcUrl(opts.baseUrl, opts.prefix, opts.segments ?? [], opts.query);
const url = buildNcUrl(
opts.baseUrl,
opts.prefix,
opts.segments ?? [],
opts.query,
opts.trailingSlash ?? false,
);
const origin = new URL(url).origin;
if (opts.credentialKey && gate.isDead(opts.credentialKey)) {