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:
@@ -65,7 +65,7 @@ export class NextcloudFilesRequestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function failure(res: Response): Promise<NextcloudFilesRequestError> {
|
||||
export async function requestFailure(res: Response): Promise<NextcloudFilesRequestError> {
|
||||
let message = `Request failed (${res.status})`;
|
||||
let code: string | null = null;
|
||||
let extra: Record<string, unknown> = {};
|
||||
@@ -91,6 +91,11 @@ async function failure(res: Response): Promise<NextcloudFilesRequestError> {
|
||||
return new NextcloudFilesRequestError(res.status, code, message, extra);
|
||||
}
|
||||
|
||||
/** Vollstaendige Adresse einer Route dieses Moduls (fuer Aufrufe, die nicht ueber `request` laufen). */
|
||||
export function nextcloudFilesUrl(path: string): string {
|
||||
return `${API_URL}${BASE}${path}`;
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
init: { method?: string; json?: unknown } = {},
|
||||
@@ -109,7 +114,7 @@ async function request<T>(
|
||||
body,
|
||||
...(method === 'GET' ? { cache: 'no-store' as const } : {}),
|
||||
});
|
||||
if (!res.ok) throw await failure(res);
|
||||
if (!res.ok) throw await requestFailure(res);
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
@@ -227,3 +232,25 @@ export function previewUrl(fileId: string, etag: string | null): string {
|
||||
const version = etag ? `&v=${encodeURIComponent(etag)}` : '';
|
||||
return `${API_URL}${BASE}/preview?fileId=${encodeURIComponent(fileId)}${version}`;
|
||||
}
|
||||
|
||||
// --- Herunterladen (Aufgabe 4) ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adresse zum Herunterladen einer Datei — oder mit `zip` eines Ordners als ZIP. Ein
|
||||
* Link in `<a href>` genuegt (Tessera-Cookie); die API setzt `Content-Disposition:
|
||||
* attachment`, der Browser speichert also nur und zeigt nichts an. Der Pfad geht nur
|
||||
* in der Query.
|
||||
*/
|
||||
export function downloadUrl(path: string, opts: { zip?: boolean } = {}): string {
|
||||
const zip = opts.zip ? '&zip=1' : '';
|
||||
return `${API_URL}${BASE}/download?path=${encodeURIComponent(path)}${zip}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adresse zum Herunterladen mehrerer Eintraege eines Ordners als ein ZIP. Jeder Name
|
||||
* steht als eigener `name`-Parameter (wiederholter Schluessel), einzeln codiert.
|
||||
*/
|
||||
export function zipUrl(dir: string, names: readonly string[]): string {
|
||||
const list = names.map((n) => `name=${encodeURIComponent(n)}`).join('&');
|
||||
return `${API_URL}${BASE}/download/zip?dir=${encodeURIComponent(dir)}&${list}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user