Step 1: complete webdav.ts — XML builders + parsers for file operations

- All PROPFIND builders (standard, extended, trashbin, versions, quota)
- PROPPATCH builder for property updates (favorites)
- REPORT builder for favorite filtering
- SEARCH builder (rfc5323) with dynamic where-clause construction
- All response parsers (files, single file, search, trashbin, versions, quota)
- Helper extractors for properties, booleans, numerics
- Namespace handling for both prefixed and unprefixed XML tags
This commit is contained in:
2026-05-11 14:08:23 +02:00
parent 84c5bdd90e
commit 73d96b9902
+79 -29
View File
@@ -4,8 +4,9 @@
import { SearchOptions, FileMetadata, TrashedFile, FileVersion, QuotaInfo } from "./types.js"; import { SearchOptions, FileMetadata, TrashedFile, FileVersion, QuotaInfo } from "./types.js";
import { normalizePath } from "./utils.js"; import { normalizePath } from "./utils.js";
// --- XML Builders (scaffolding) --- // --- XML Builders ---
/** PROPFIND standard — richiede il set di proprietà di default o quelle specificate */
export function buildPropfindBody(selectedProps?: string[]): string { export function buildPropfindBody(selectedProps?: string[]): string {
const props = selectedProps && selectedProps.length > 0 const props = selectedProps && selectedProps.length > 0
? selectedProps.map((p) => ` <${p} />`).join("\n") ? selectedProps.map((p) => ` <${p} />`).join("\n")
@@ -28,6 +29,7 @@ ${props}
</d:propfind>`; </d:propfind>`;
} }
/** PROPFIND esteso — per get_file_info (include owner, checksum, has-preview) */
export function buildPropfindExtendedBody(): string { export function buildPropfindExtendedBody(): string {
return `<?xml version="1.0" encoding="UTF-8"?> return `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
@@ -50,6 +52,7 @@ export function buildPropfindExtendedBody(): string {
</d:propfind>`; </d:propfind>`;
} }
/** PROPFIND per cestino — proprietà trashbin-specific */
export function buildTrashbinPropfindBody(): string { export function buildTrashbinPropfindBody(): string {
return `<?xml version="1.0" encoding="UTF-8"?> return `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
@@ -70,6 +73,7 @@ export function buildTrashbinPropfindBody(): string {
</d:propfind>`; </d:propfind>`;
} }
/** PROPFIND per versioni file */
export function buildVersionsPropfindBody(): string { export function buildVersionsPropfindBody(): string {
return `<?xml version="1.0" encoding="UTF-8"?> return `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
@@ -82,17 +86,19 @@ export function buildVersionsPropfindBody(): string {
</d:propfind>`; </d:propfind>`;
} }
/** PROPPATCH generico — imposta una proprietà su una risorsa */
export function buildProppatchBody(namespace: string, property: string, value: string): string { export function buildProppatchBody(namespace: string, property: string, value: string): string {
return `<?xml version="1.0" encoding="UTF-8"?> return `<?xml version="1.0" encoding="UTF-8"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:set> <d:set>
<d:prop> <d:prop>
<${namespace}:${property}>${value}</${namespace}:${property}> <${namespace}:${property}>${escapeXml(value)}</${namespace}:${property}>
</d:prop> </d:prop>
</d:set> </d:set>
</d:propertyupdate>`; </d:propertyupdate>`;
} }
/** REPORT — filtro per file preferiti */
export function buildFavoriteFilterBody(selectedProps?: string[]): string { export function buildFavoriteFilterBody(selectedProps?: string[]): string {
const props = selectedProps && selectedProps.length > 0 const props = selectedProps && selectedProps.length > 0
? selectedProps.map((p) => ` <${p} />`).join("\n") ? selectedProps.map((p) => ` <${p} />`).join("\n")
@@ -118,6 +124,18 @@ ${props}
</oc:filter-files>`; </oc:filter-files>`;
} }
/** PROPFIND per quota — richiede d:quota-used-bytes e d:quota-available-bytes */
export function buildQuotaBody(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<d:quota-used-bytes />
<d:quota-available-bytes />
</d:prop>
</d:propfind>`;
}
/** SEARCH request generica (rfc5323) — costruisce dinamicamente il where-clause */
export function buildSearchRequest(options: SearchOptions): string { export function buildSearchRequest(options: SearchOptions): string {
const filters: string[] = []; const filters: string[] = [];
@@ -225,72 +243,104 @@ ${limit}
// --- XML Parsers (regex-based, like caldav.ts) --- // --- XML Parsers (regex-based, like caldav.ts) ---
function extractProperty(block: string, namespace: string, property: string): string | null { /** Estrae il valore testuale di una proprietà XML da un blocco <response>.
const regex = new RegExp(`<(?:${namespace}:)?${property}[^>]*>([^<]*)</(?:${namespace}:)?${property}>`, "i"); * Gestisce namespace con prefisso (<d:getcontenttype>) e senza prefisso
* (<getcontenttype xmlns="DAV:">). */
export function extractProperty(block: string, namespace: string, property: string): string | null {
const regex = new RegExp(`<(?:${namespace}:)?${property}\\b[^>]*>([^<]*)</(?:${namespace}:)?${property}>`, "i");
const match = block.match(regex); const match = block.match(regex);
return match ? decodeXmlText(match[1]) : null; return match ? decodeXmlText(match[1]) : null;
} }
function extractNumericProperty(block: string, namespace: string, property: string): number | null { /** Estrae una proprietà numerica; restituisce null se mancante o non valida. */
export function extractNumericProperty(block: string, namespace: string, property: string): number | null {
const val = extractProperty(block, namespace, property); const val = extractProperty(block, namespace, property);
if (val === null || val === "") return null; if (val === null || val === "") return null;
const num = Number(val); const num = Number(val);
return Number.isFinite(num) ? num : null; return Number.isFinite(num) ? num : null;
} }
function extractBooleanProperty(block: string, namespace: string, property: string): boolean { /** Estrae una proprietà booleana (1/true = true, tutto il resto = false). */
export function extractBooleanProperty(block: string, namespace: string, property: string): boolean {
const val = extractProperty(block, namespace, property); const val = extractProperty(block, namespace, property);
return val === "1" || val?.toLowerCase() === "true"; return val === "1" || val?.toLowerCase() === "true";
} }
function hasCollection(block: string): boolean { /** Verifica se il blocco contiene un <collection> (cartella). */
export function hasCollection(block: string): boolean {
return /<(?:\w+:)?collection\b/i.test(block); return /<(?:\w+:)?collection\b/i.test(block);
} }
function extractHref(block: string): string | null { /** Estrae l'href da un blocco <response>. */
const match = block.match(/<(?:\w+:)?href[^>]*>([\s\S]*?)<\/(?:\w+:)?href>/); export function extractHref(block: string): string | null {
const match = block.match(/<(?:\w+:)?href\b[^>]*>([\s\S]*?)<\/(?:\w+:)?href>/);
return match ? decodeXmlText(match[1].trim()) : null; return match ? decodeXmlText(match[1].trim()) : null;
} }
/** Converte un blocco <response> in un oggetto FileMetadata. */
function parseFileMetadataFromBlock(block: string, basePath: string): FileMetadata | null {
const href = extractHref(block);
if (!href) return null;
const name = extractProperty(block, "d", "displayname") || "";
const isFolder = hasCollection(block);
return {
name,
path: resolveRelativePathFromHref(href, basePath),
type: isFolder ? "folder" : "file",
size: extractNumericProperty(block, "oc", "size") ?? undefined,
contentLength: extractNumericProperty(block, "d", "getcontentlength") ?? undefined,
mimeType: extractProperty(block, "d", "getcontenttype") ?? undefined,
lastModified: extractProperty(block, "d", "getlastmodified") ?? undefined,
etag: extractProperty(block, "d", "getetag") ?? undefined,
fileId: extractNumericProperty(block, "oc", "fileid") ?? undefined,
permissions: extractProperty(block, "oc", "permissions") ?? undefined,
favorite: extractBooleanProperty(block, "oc", "favorite"),
ownerId: extractProperty(block, "oc", "owner-id") ?? undefined,
ownerDisplayName: extractProperty(block, "oc", "owner-display-name") ?? undefined,
hasPreview: extractBooleanProperty(block, "nc", "has-preview"),
};
}
/** PROPFIND response → array FileMetadata.
* ESCLUDE il primo elemento (la cartella root stessa). */
export function parsePropfindFilesResponse(xml: string, basePath: string): FileMetadata[] { export function parsePropfindFilesResponse(xml: string, basePath: string): FileMetadata[] {
const files: FileMetadata[] = []; const files: FileMetadata[] = [];
const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g); const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g);
let isFirst = true;
for (const match of responseMatches) { for (const match of responseMatches) {
const block = match[0]; const block = match[0];
const href = extractHref(block); if (isFirst) {
if (!href) continue; isFirst = false;
continue; // Skip root folder
}
const name = extractProperty(block, "d", "displayname") || ""; const meta = parseFileMetadataFromBlock(block, basePath);
const isFolder = hasCollection(block); if (meta) files.push(meta);
files.push({
name,
path: resolveRelativePathFromHref(href, basePath),
type: isFolder ? "folder" : "file",
size: extractNumericProperty(block, "oc", "size") ?? undefined,
contentLength: extractNumericProperty(block, "d", "getcontentlength") ?? undefined,
mimeType: extractProperty(block, "d", "getcontenttype") ?? undefined,
lastModified: extractProperty(block, "d", "getlastmodified") ?? undefined,
etag: extractProperty(block, "d", "getetag") ?? undefined,
fileId: extractNumericProperty(block, "oc", "fileid") ?? undefined,
permissions: extractProperty(block, "oc", "permissions") ?? undefined,
favorite: extractBooleanProperty(block, "oc", "favorite"),
});
} }
return files; return files;
} }
/** PROPFIND Depth:0 response → singolo FileMetadata.
* Non esclude il primo elemento (è l'unico elemento richiesto). */
export function parsePropfindSingleFileResponse(xml: string, basePath: string): FileMetadata | null { export function parsePropfindSingleFileResponse(xml: string, basePath: string): FileMetadata | null {
const files = parsePropfindFilesResponse(xml, basePath); const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g);
return files[0] || null; for (const match of responseMatches) {
const meta = parseFileMetadataFromBlock(match[0], basePath);
if (meta) return meta;
}
return null;
} }
export function parseSearchResponse(xml: string, basePath: string): FileMetadata[] { export function parseSearchResponse(xml: string, basePath: string): FileMetadata[] {
return parsePropfindFilesResponse(xml, basePath); return parsePropfindFilesResponse(xml, basePath);
} }
/** PROPFIND trashbin → array TrashedFile.
* Il primo elemento è il cestino stesso; viene incluso (diverso da parsePropfindFilesResponse). */
export function parseTrashbinResponse(xml: string): TrashedFile[] { export function parseTrashbinResponse(xml: string): TrashedFile[] {
const files: TrashedFile[] = []; const files: TrashedFile[] = [];
const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g); const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g);