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
+64
View File
@@ -163,3 +163,67 @@ export function cancelLoginFlow(flowId: string): Promise<{ cancelled: true }> {
export function disconnectNextcloud(): Promise<{ disconnected: true }> {
return request<{ disconnected: true }>('/connect', { method: 'DELETE' });
}
// --- Dateien (Etappe 1, Aufgabe 3) -------------------------------------------------------------
export interface NcEntry {
name: string;
/** Pfad ab der Wurzel, z. B. `/Projekte/Bericht.pdf`. */
path: string;
type: 'folder' | 'file';
size: number;
mime: string | null;
/** ISO-Zeitstempel. */
mtime: string | null;
etag: string | null;
fileId: string | null;
/** Berechtigungsbuchstaben der Nextcloud (R teilbar, D loeschbar, N umbenennbar ...). */
permissions: string;
hasPreview: boolean;
favorite: boolean;
}
export interface NcQuota {
used: number;
/** null = unbegrenzt oder unbekannt. */
available: number | null;
}
export interface NcListing {
path: string;
entries: NcEntry[];
quota: NcQuota;
/** true, wenn der Ordner mehr als 5000 Eintraege hat und abgeschnitten wurde. */
truncated: boolean;
}
/** Ordnerinhalt samt Speicherangaben. Der Pfad geht nur in der Query, nie im URL-Pfad. */
export function listFolder(path: string): Promise<NcListing> {
return request<NcListing>(`/files?path=${encodeURIComponent(path)}`);
}
export function createFolder(path: string): Promise<{ path: string }> {
return request<{ path: string }>('/folders', { method: 'POST', json: { path } });
}
/** Verschieben oder Umbenennen; ein vorhandener Name im Ziel wird nie ueberschrieben (nameTaken). */
export function moveEntry(from: string, to: string): Promise<{ from: string; to: string }> {
return request<{ from: string; to: string }>('/move', { method: 'POST', json: { from, to } });
}
/** Loescht in den Papierkorb der Nextcloud. */
export function deleteEntry(path: string): Promise<{ deleted: true }> {
return request<{ deleted: true }>(`/files?path=${encodeURIComponent(path)}`, {
method: 'DELETE',
});
}
/**
* Adresse des Vorschaubildes fuer ein `<img>` (Tessera-Cookie genuegt). Die
* Version (Entity-Tag) macht die Adresse je Dateiversion eindeutig und erlaubt
* dem Browser, das Bild zu cachen.
*/
export function previewUrl(fileId: string, etag: string | null): string {
const version = etag ? `&v=${encodeURIComponent(etag)}` : '';
return `${API_URL}${BASE}/preview?fileId=${encodeURIComponent(fileId)}${version}`;
}